moshcode 0.66.0 → 0.68.0

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
@@ -48,6 +48,7 @@ or miss one that does. A test fails the build when it drifts.
48
48
  | `moshcode load` | account | bring your saved pit settings onto this machine |
49
49
  | `moshcode console` | account | serve or connect to the browser terminal |
50
50
  | `moshcode dns` | hosting | resolve Moshpit names on this machine |
51
+ | `moshcode name` | hosting | prove you hold a Moshpit name, so an app can use it as your identity |
51
52
  | `moshcode doh` | hosting | run the DNS-over-HTTPS resolver |
52
53
  | `moshcode site` <br>`serve` | hosting | install web-server config for a Moshpit name |
53
54
  | `moshcode template` <br>`templates` | hosting | scaffold a stack for a Moshpit-hosted service |
@@ -590,6 +591,40 @@ moshcode ugig --json gigs list # arguments/output go straight to ugig
590
591
  moshcode coinpay wallet balance # arguments/output go straight to coinpay
591
592
  ```
592
593
 
594
+ ### BufferOverride — the failure in front of you, already answered
595
+
596
+ [BufferOverride](https://bufferoverride.com) is where humans and agents debug
597
+ together: every answer declares the versions it works on, who or what wrote it,
598
+ and whether anyone independent reproduced it. `bo` is that from a terminal.
599
+
600
+ ```sh
601
+ moshcode install bo # npm i -g @profullstack/bufferoverride
602
+
603
+ moshcode bo run -- pnpm test # run it, keep what it printed, search for it
604
+ moshcode bo search "worker exited before finishing"
605
+ moshcode bo get a1b2c3d4e5 --markdown
606
+ ```
607
+
608
+ The product is BufferOverride and the binary is `bo` — the same split
609
+ `secrets`/`logicsrc` and `spinifex`/`spx` have, keyed the short way round here
610
+ because this is a command you type every time something fails.
611
+
612
+ `bo run --` wraps a command rather than replacing it: the wrapped command's exit
613
+ code passes straight through, so it can go in front of something already in CI
614
+ without changing what CI sees. It captures stdout, stderr, the exit code, the
615
+ OS, the architecture and the detected dependency versions, redacts what it
616
+ recognises as a secret, and searches for the failure **before** offering to
617
+ publish it. Nothing leaves the machine until you have seen it, and outside a TTY
618
+ nothing is published at all unless you pass `--ask`.
619
+
620
+ Redaction is best effort and cannot be complete — no pattern list catches a
621
+ custom-format secret — so `--dry-run` is the habit its own docs ask for.
622
+
623
+ Reads need no credential: `search` and `get` work before you have ever run `bo
624
+ login`. Publishing needs one, and `bo login` is a device-code exchange, so a
625
+ terminal never handles a browser session. `bo mcp config` prints the MCP
626
+ registration for a coding agent, which is the same graph over a different door.
627
+
593
628
  ### Cloud + infra CLIs
594
629
 
595
630
  ```sh
@@ -665,6 +700,35 @@ engines, `mcpjam` tells you whether that server is healthy first — health
665
700
  checks, OAuth conformance, tool-surface diffing, and structured triage from the
666
701
  terminal or CI. Re-running `moshcode install mcpjam` is also its upgrade path.
667
702
 
703
+ ### ElevenLabs — Eleven Agents, voices, and speech
704
+
705
+ ```sh
706
+ moshcode install elevenlabs # npm i -g @elevenlabs/cli
707
+
708
+ moshcode elevenlabs auth login # PKCE OAuth, stored in the system keyring
709
+ moshcode elevenlabs agents list
710
+ moshcode elevenlabs agents push # upload local agent configs to the platform
711
+ moshcode elevenlabs text-to-speech convert …
712
+ ```
713
+
714
+ [ElevenLabs](https://elevenlabs.io/docs/eleven-agents/operate/cli)' CLI manages
715
+ **Eleven Agents** — conversational voice agents you define in files and
716
+ `push`/`pull` against their platform, alongside the knowledge bases, tools,
717
+ tests and phone numbers those agents use. The same binary reaches the rest of
718
+ the API: voices, text-to-speech, dubbing, transcription, music, and workspace
719
+ usage.
720
+
721
+ They are agents in a different sense than the ones under `/agents`: you deploy
722
+ them and callers talk to them, rather than handing your terminal to one. That is
723
+ why it sits here with the workflow CLIs — every subcommand runs one request and
724
+ exits, so it pipes like the rest of the roster. `--format json` (the default
725
+ when stdout is not a TTY) and `--query` (JMESPath) are what a script wants.
726
+
727
+ The npm package is a small shim over a native binary shipped as an optional
728
+ dependency, so install it without `--omit=optional` or the `elevenlabs` on your
729
+ PATH will refuse to run. Re-running `moshcode install elevenlabs` is its upgrade
730
+ path.
731
+
668
732
  ### Alchemy — onchain data, wallets, and x402
669
733
 
670
734
  ```sh
@@ -1038,6 +1102,7 @@ Some MCP servers are worth remembering by name rather than by npx invocation:
1038
1102
  ```sh
1039
1103
  moshcode mcp catalog # what we know how to run
1040
1104
  moshcode mcp add porkbun # expands to: npx -y @porkbunllc/mcp-server
1105
+ moshcode mcp add bufferoverride # expands to: https://bufferoverride.com/mcp
1041
1106
  ```
1042
1107
 
1043
1108
  That registers it across every engine that supports MCP (claude, gemini, qwen,
@@ -1059,6 +1124,23 @@ from and five to rotate. Porkbun's API access is off by default and enabled
1059
1124
  per-domain — and its documentation tools work with no keys at all, which is a
1060
1125
  sensible way to try the server before trusting it with DNS writes.
1061
1126
 
1127
+ **BufferOverride is the useful-unauthenticated one.** Five read tools
1128
+ (`search_questions`, `get_question`, `list_questions`, `list_tags`, `whoami`)
1129
+ work with no credential, and the write tools are gated on the scopes a key
1130
+ actually carries — `tools/list` advertises only what your key can use. So the
1131
+ bare `mcp add bufferoverride` above is a complete, working registration. To
1132
+ publish from an engine, add the credential as a header:
1133
+
1134
+ ```sh
1135
+ moshcode mcp add bufferoverride -H "Authorization: Bearer bo_..."
1136
+ ```
1137
+
1138
+ `bo mcp config` prints the same thing from a terminal that has already signed
1139
+ in, and `bo mcp config --no-token` prints a form safe to paste in public. It is
1140
+ registered under the name the CLI uses, so both routes produce one server rather
1141
+ than two. See [BufferOverride](#bufferoverride--the-failure-in-front-of-you-already-answered)
1142
+ above for the CLI itself.
1143
+
1062
1144
  ## Claude Code plugins
1063
1145
 
1064
1146
  MoshCode publishes its own plugin marketplace, so the pit's slash commands work
@@ -1371,6 +1453,7 @@ chmod +x deploy.mosh
1371
1453
  | `coinpay(args…)` | drive the coinpay workflow CLI |
1372
1454
  | `c0mpute(args…)` | drive the c0mpute workflow CLI |
1373
1455
  | `c0upons(args…)` | drive the c0upons workflow CLI |
1456
+ | `bo(args…)` | drive the BufferOverride CLI (capture a failure, search, ask, answer, verify) |
1374
1457
  | `secrets(args…)` | drive the logicsrc secrets CLI |
1375
1458
  | `railway(args…)` | drive the Railway CLI |
1376
1459
  | `gh(args…)` | drive the GitHub CLI |
@@ -1384,6 +1467,7 @@ chmod +x deploy.mosh
1384
1467
  | `mcpjam(args…)` | drive the MCPJam CLI (test, debug, and validate MCP servers) |
1385
1468
  | `spinifex(args…)` | drive the Spinifex CLI (`spx` — AWS-compatible cloud on your own hardware) |
1386
1469
  | `alchemy(args…)` | drive the Alchemy CLI (onchain data, apps, wallets, x402) |
1470
+ | `elevenlabs(args…)` | drive the ElevenLabs CLI (Eleven Agents, voices, TTS, dubbing) |
1387
1471
  | `trade(args…)` | look up tickers, inspect markets, preview/place Alpaca orders |
1388
1472
  | `stocks(args…)` | research tickers via advis0r (`stocksRead` returns the data) |
1389
1473
  | `crypto(args…)` | research crypto pairs via advis0r (`cryptoRead` returns the data) |
package/bin/moshcode.mjs CHANGED
@@ -33,6 +33,7 @@ import { consoleCommand } from "../src/console.mjs";
33
33
  import { herdCommand, herdStart, splitDetachArgs } from "../src/herd-cli.mjs";
34
34
  import { detectSubstrate, substrateNote } from "../src/herd.mjs";
35
35
  import { dnsCommand } from "../src/dns.mjs";
36
+ import { nameCommand } from "../src/name-link.mjs";
36
37
  import { templateCommand } from "../src/templates.mjs";
37
38
  import { serveCommand } from "../src/serve.mjs";
38
39
  import { createDohServer, nginxDohSite, parseDohPort, parseGuardArgs, DEFAULT_DOH_PORT, DOH_PATH } from "../src/doh-server.mjs";
@@ -545,6 +546,10 @@ async function main() {
545
546
  process.exitCode = (await dnsCommand(rest)) || 0;
546
547
  return;
547
548
  }
549
+ if (cmd === "name") {
550
+ process.exitCode = (await nameCommand(rest)) || 0;
551
+ return;
552
+ }
548
553
  if (cmd === "doh") {
549
554
  const nameAt = rest.indexOf("--nginx");
550
555
  if (nameAt >= 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moshcode",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "type": "module",
5
5
  "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
6
6
  "repository": {
@@ -394,6 +394,22 @@ export const CORE_CLI_COMMANDS = [
394
394
  ],
395
395
  seeAlso: ["doh", "site"],
396
396
  },
397
+ {
398
+ name: "name",
399
+ group: "hosting",
400
+ description: "prove you hold a Moshpit name, so an app can use it as your identity",
401
+ synopsis: [["moshcode name link <name> [--app <url>]", ""]],
402
+ flags: [
403
+ ["--app <url>", "the app to prove the name to", "https://qrypt.chat"],
404
+ ["--dir <path>", "where the name's key and certificate live", "/etc/ssl/moshpit"],
405
+ ["--json", "print the proof bundle as machine-readable JSON", ""],
406
+ ],
407
+ examples: [
408
+ ["sudo moshcode name link chovy.hacker", "the key is root-owned"],
409
+ ["sudo moshcode name link blue.eggs --json", "hand the bundle to a script"],
410
+ ],
411
+ seeAlso: ["dns", "site", "whoami"],
412
+ },
397
413
  {
398
414
  name: "doh",
399
415
  group: "hosting",
package/src/commands.mjs CHANGED
@@ -668,6 +668,7 @@ const COMMANDS = [
668
668
  cliVerb("coinpay", "drive the coinpay workflow CLI"),
669
669
  cliVerb("c0mpute", "drive the c0mpute workflow CLI"),
670
670
  cliVerb("c0upons", "drive the c0upons workflow CLI"),
671
+ cliVerb("bo", "drive the BufferOverride CLI (capture a failure, search, ask, answer, verify)"),
671
672
  cliVerb("secrets", "manage/view team secrets via logicsrc (login, teams, credentials)"),
672
673
  cliVerb("railway", "drive the Railway CLI (deploys, services, env vars)"),
673
674
  cliVerb("gh", "drive the GitHub CLI (repos, PRs, issues, releases)"),
@@ -682,6 +683,7 @@ const COMMANDS = [
682
683
  cliVerb("mcpjam", "drive the MCPJam CLI (test, debug, and validate MCP servers)"),
683
684
  cliVerb("spinifex", "drive the Spinifex CLI (spx — AWS-compatible cloud on your own hardware)"),
684
685
  cliVerb("alchemy", "drive the Alchemy CLI (onchain data, apps, wallets, x402)"),
686
+ cliVerb("elevenlabs", "drive the ElevenLabs CLI (Eleven Agents, voices, TTS, dubbing)"),
685
687
  cliVerb("trade", "look up tickers, inspect markets, and preview/place Alpaca orders"),
686
688
  cliVerb("pwd", "print the current repo/location"),
687
689
 
@@ -238,9 +238,14 @@ export async function mcpCommand(tokens, { run, installedSet } = {}) {
238
238
  const missing = (parsed.catalog?.env || []).filter((k) => !process.env[k]);
239
239
  if (missing.length) {
240
240
  console.log(ash(` note: ${spec.name} needs ${missing.join(" and ")} in the environment.`));
241
- if (parsed.catalog?.note) console.log(ash(` ${parsed.catalog.note}`));
242
- if (parsed.catalog?.docs) console.log(ash(` ${parsed.catalog.docs}`));
243
241
  }
242
+ // The catalog's own note and docs are printed whenever the catalog was used,
243
+ // not only when a variable is missing. A server whose credential is a header
244
+ // rather than an environment variable — or one that needs none at all to be
245
+ // useful — has nothing in `env`, and hanging its note off that check is what
246
+ // made the note invisible for exactly the servers it was written for.
247
+ if (parsed.catalog?.note) console.log(ash(` note: ${parsed.catalog.note}`));
248
+ if (parsed.catalog?.docs) console.log(ash(` ${parsed.catalog.docs}`));
244
249
  if (spec.headers.length || /^https?:/i.test(spec.target)) {
245
250
  console.log(ash(" note: OAuth/HTTP servers may still need per-engine auth (e.g. `opencode mcp auth`, `codex mcp login`)."));
246
251
  }
@@ -24,6 +24,23 @@ export const MCP_CATALOG = {
24
24
  // before deciding whether to trust it with keys.
25
25
  note: "API access is off by default and must be enabled per-domain; the docs tools work without keys",
26
26
  },
27
+ bufferoverride: {
28
+ // A remote HTTP server, so the target is the URL and there are no args —
29
+ // every engine's builder pushes the target alone for a remote server.
30
+ target: "https://bufferoverride.com/mcp",
31
+ args: [],
32
+ desc: "BufferOverride — version-aware technical answers, with provenance and reproductions",
33
+ docs: "https://bufferoverride.com/docs/mcp",
34
+ // Named to match what the CLI's own `bo mcp config` emits, so registering
35
+ // it either way produces one server rather than two under different names.
36
+ //
37
+ // No `env`: the credential is a bearer header, not a variable, and it is
38
+ // deliberately not listed here. Five read tools work with no key at all,
39
+ // and the write tools are gated on the scopes a key actually carries — so
40
+ // the useful default really is unauthenticated. `bo mcp config` prints the
41
+ // header form for a terminal that has signed in.
42
+ note: "reads need no credential; to publish, add -H \"Authorization: Bearer bo_…\" (see `bo mcp config`)",
43
+ },
27
44
  };
28
45
 
29
46
  /** Resolve a catalog name to a spec fragment, or null. Own properties only. */
@@ -44,7 +61,10 @@ export function catalogNames() {
44
61
 
45
62
  /** One line per known server, for `mcp catalog`. */
46
63
  export function catalogList() {
64
+ // Width from the longest name rather than a fixed pad: `bufferoverride` is
65
+ // wider than the old 10, and a name that overruns the pad loses the column.
66
+ const width = Math.max(10, ...Object.keys(MCP_CATALOG).map((key) => key.length));
47
67
  return Object.entries(MCP_CATALOG)
48
- .map(([key, e]) => ` ${key.padEnd(10)} ${e.desc}`)
68
+ .map(([key, e]) => ` ${key.padEnd(width)} ${e.desc}`)
49
69
  .join("\n");
50
70
  }
@@ -0,0 +1,259 @@
1
+ // `moshcode name link <name>` — prove you hold a Moshpit name, so an app can
2
+ // use it as your identity.
3
+ //
4
+ // The registry already publishes, per name, the SHA-256 of the SubjectPublicKeyInfo
5
+ // that name's certificate must present (see src/pins.mjs). That pin is a
6
+ // name-to-key binding nobody else can forge, which makes it a credential: sign
7
+ // something with the pinned key and you have proved you hold the name.
8
+ //
9
+ // What this does NOT do is generate the app's encryption key. The pinned key is
10
+ // P-256 and it signs; a messenger's key is ML-KEM-1024 and it encrypts, and a
11
+ // KEM key cannot sign at all. They are two keys with two jobs, and the private
12
+ // half of the second one belongs on the device the person actually reads
13
+ // messages on — not here. So this emits a proof bundle and stops. The app posts
14
+ // it alongside a key it generated itself and never showed anyone.
15
+ //
16
+ // The bundle is single-use and short-lived: the challenge it answers is burned
17
+ // on redemption, so a copy of it is worth nothing once used.
18
+
19
+ import fs from "node:fs";
20
+ import crypto from "node:crypto";
21
+
22
+ /** Where an app that speaks this protocol lives, unless told otherwise. */
23
+ export const DEFAULT_APP = "https://qrypt.chat";
24
+
25
+ /** Where moshcode's own tooling writes a name's key and certificate. */
26
+ export const DEFAULT_KEY_DIR = "/etc/ssl/moshpit";
27
+
28
+ /**
29
+ * A name is `<label>.<tld>`, lowercase.
30
+ *
31
+ * Empty segments are refused rather than collapsed. `chovy..hacker` folding
32
+ * into `chovy.hacker` is harmless in a filename and is a way in when the string
33
+ * is an identity.
34
+ *
35
+ * @param {unknown} input
36
+ * @returns {string | null}
37
+ */
38
+ export function normalizeName(input) {
39
+ const clean = String(input ?? "").trim().toLowerCase().replace(/\.$/, "");
40
+ if (!clean) return null;
41
+ const parts = clean.split(".");
42
+ if (parts.length !== 2 || parts.some((p) => !p)) return null;
43
+ if (!parts.every((p) => /^[a-z0-9-]+$/.test(p) && !p.startsWith("-") && !p.endsWith("-"))) return null;
44
+ return parts.join(".");
45
+ }
46
+
47
+ /**
48
+ * Where a name's key and certificate live.
49
+ *
50
+ * Mirrors keyPaths() in src/pins.mjs, including that separators which could
51
+ * climb out of the directory are dropped rather than escaped.
52
+ *
53
+ * @param {string} name
54
+ * @param {string} [dir]
55
+ */
56
+ export function keyPaths(name, dir = DEFAULT_KEY_DIR) {
57
+ const safe = String(name ?? "")
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9.-]/g, "")
60
+ .replace(/\.{2,}/g, ".")
61
+ .replace(/^[.-]+|[.-]+$/g, "");
62
+ if (!safe) return null;
63
+ return { key: `${dir}/${safe}.key`, cert: `${dir}/${safe}.crt` };
64
+ }
65
+
66
+ /**
67
+ * Sign a challenge with a name's private key.
68
+ *
69
+ * DER, because that is what `crypto.verify` reads back with `dsaEncoding: 'der'`
70
+ * on the other side. A P-1363 signature is the same numbers in a shape the
71
+ * verifier rejects, and the failure looks identical to a wrong key.
72
+ *
73
+ * @param {{keyPem: string, nonce: string}} args
74
+ * @returns {string} base64 signature
75
+ */
76
+ export function signChallenge({ keyPem, nonce }) {
77
+ const key = crypto.createPrivateKey(keyPem);
78
+ return crypto
79
+ .sign("sha256", Buffer.from(nonce, "utf8"), { key, dsaEncoding: "der" })
80
+ .toString("base64");
81
+ }
82
+
83
+ /**
84
+ * The pin a certificate's key hashes to — SHA-256 over the SPKI, base64.
85
+ * Shown so the operator can eyeball it against what the registry publishes
86
+ * before wondering why a proof was refused.
87
+ *
88
+ * @param {string} certPem
89
+ */
90
+ export function pinOf(certPem) {
91
+ const cert = new crypto.X509Certificate(certPem);
92
+ const der = cert.publicKey.export({ type: "spki", format: "der" });
93
+ return crypto.createHash("sha256").update(der).digest("base64");
94
+ }
95
+
96
+ /**
97
+ * Ask an app for a challenge to sign.
98
+ *
99
+ * @param {{app: string, name: string, fetchImpl?: typeof fetch}} args
100
+ */
101
+ export async function fetchChallenge({ app, name, fetchImpl = fetch }) {
102
+ const url = `${app.replace(/\/+$/, "")}/api/auth/moshpit/challenge`;
103
+ const res = await fetchImpl(url, {
104
+ method: "POST",
105
+ headers: { "content-type": "application/json" },
106
+ body: JSON.stringify({ name }),
107
+ });
108
+
109
+ let body = null;
110
+ try {
111
+ body = await res.json();
112
+ } catch {
113
+ body = null;
114
+ }
115
+
116
+ if (!res.ok) {
117
+ throw new Error(body?.error || `${app} answered ${res.status}`);
118
+ }
119
+ if (!body?.jti || !body?.nonce) {
120
+ throw new Error(`${app} did not return a challenge`);
121
+ }
122
+ return body;
123
+ }
124
+
125
+ /**
126
+ * Read a name's key and certificate off disk, with the failure a person can act on.
127
+ *
128
+ * The key is root-owned, so "permission denied" is the expected first
129
+ * experience and deserves the fix rather than the errno.
130
+ *
131
+ * @param {{name: string, keyPath: string, certPath: string, readFile?: (p: string, e: string) => string}} args
132
+ */
133
+ export function readNameKey({ name, keyPath, certPath, readFile = (p, e) => fs.readFileSync(p, e) }) {
134
+ let keyPem;
135
+ try {
136
+ keyPem = readFile(keyPath, "utf8");
137
+ } catch (error) {
138
+ if (error.code === "EACCES") {
139
+ throw new Error(`cannot read ${keyPath} — it is root-owned, so run this with sudo`);
140
+ }
141
+ if (error.code === "ENOENT") {
142
+ throw new Error(`no key for ${name} at ${keyPath} — mint one before linking it`);
143
+ }
144
+ throw error;
145
+ }
146
+
147
+ let certPem;
148
+ try {
149
+ certPem = readFile(certPath, "utf8");
150
+ } catch (error) {
151
+ if (error.code === "ENOENT") {
152
+ throw new Error(`no certificate for ${name} at ${certPath}`);
153
+ }
154
+ throw error;
155
+ }
156
+
157
+ return { keyPem, certPem };
158
+ }
159
+
160
+ /**
161
+ * Parse `name link <name> [--app url] [--dir path] [--json]`.
162
+ * @param {string[]} argv
163
+ */
164
+ export function parseArgs(argv) {
165
+ const flag = (name, fallback) => {
166
+ const at = argv.indexOf(`--${name}`);
167
+ return at >= 0 && argv[at + 1] ? argv[at + 1] : fallback;
168
+ };
169
+ const positional = argv.filter((a, i) => {
170
+ if (a.startsWith("--")) return false;
171
+ // Skip a value that belongs to the flag before it.
172
+ return !(i > 0 && argv[i - 1].startsWith("--") && argv[i - 1] !== "--json");
173
+ });
174
+
175
+ return {
176
+ verb: positional[0] || "",
177
+ name: positional[1] || "",
178
+ app: flag("app", DEFAULT_APP),
179
+ dir: flag("dir", DEFAULT_KEY_DIR),
180
+ json: argv.includes("--json"),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * `moshcode name link <name>` — fetch a challenge, sign it, print the bundle.
186
+ *
187
+ * @param {string[]} argv
188
+ * @param {{out?: (s: string) => void, err?: (s: string) => void, fetchImpl?: typeof fetch,
189
+ * readFile?: (p: string, e: string) => string}} [io]
190
+ * @returns {Promise<number>} exit code
191
+ */
192
+ export async function nameCommand(argv, io = {}) {
193
+ const out = io.out || ((s) => console.log(s));
194
+ const err = io.err || ((s) => console.error(s));
195
+ const { verb, name: raw, app, dir, json } = parseArgs(argv);
196
+
197
+ if (verb !== "link") {
198
+ err("usage: moshcode name link <name> [--app <url>] [--dir <path>] [--json]");
199
+ return 1;
200
+ }
201
+
202
+ const name = normalizeName(raw);
203
+ if (!name) {
204
+ err(`not a Moshpit name: ${raw || "(none)"} — expected <label>.<tld>`);
205
+ return 1;
206
+ }
207
+
208
+ const paths = keyPaths(name, dir);
209
+ if (!paths) {
210
+ err(`not a Moshpit name: ${raw}`);
211
+ return 1;
212
+ }
213
+
214
+ let keyPem;
215
+ let certPem;
216
+ try {
217
+ ({ keyPem, certPem } = readNameKey({
218
+ name,
219
+ keyPath: paths.key,
220
+ certPath: paths.cert,
221
+ readFile: io.readFile,
222
+ }));
223
+ } catch (error) {
224
+ err(error.message);
225
+ return 1;
226
+ }
227
+
228
+ let challenge;
229
+ try {
230
+ challenge = await fetchChallenge({ app, name, fetchImpl: io.fetchImpl });
231
+ } catch (error) {
232
+ err(`could not get a challenge from ${app}: ${error.message}`);
233
+ return 1;
234
+ }
235
+
236
+ let signature;
237
+ try {
238
+ signature = signChallenge({ keyPem, nonce: challenge.nonce });
239
+ } catch (error) {
240
+ err(`could not sign with ${paths.key}: ${error.message}`);
241
+ return 1;
242
+ }
243
+
244
+ const bundle = { jti: challenge.jti, name, certPem, signature };
245
+
246
+ if (json) {
247
+ out(JSON.stringify(bundle));
248
+ return 0;
249
+ }
250
+
251
+ out(`Proved ${name} against ${app}`);
252
+ out(` pin ${pinOf(certPem)}`);
253
+ out(` expires ${challenge.expiresAt || "shortly"}`);
254
+ out("");
255
+ out("Paste this into the app to finish linking. It is single-use:");
256
+ out("");
257
+ out(JSON.stringify(bundle));
258
+ return 0;
259
+ }
package/src/tools.mjs CHANGED
@@ -82,6 +82,19 @@ export const TOOLS = {
82
82
  // where those words mean something else entirely.
83
83
  aliases: { cmd: "cli-tools", args: ["aliases", "--json"] },
84
84
  },
85
+ bo: {
86
+ desc: "BufferOverride — capture a failing command, redact it, and find the answer that already exists",
87
+ // The product is BufferOverride and the binary is `bo`, the same split
88
+ // `secrets` → `logicsrc` and `spinifex` → `spx` have. It is keyed the other
89
+ // way round from those two on purpose: `bo` is what its own documentation
90
+ // tells you to type, and this is a command you run every time something
91
+ // fails, so the short word is the one worth having in the pit.
92
+ bin: "bo",
93
+ // An ordinary global npm package with no dependencies of its own, and
94
+ // `npm install -g` is idempotent, so re-running the install IS the upgrade
95
+ // — no `upgrade` key, the same as mcpjam and railway.
96
+ install: { cmd: "npm", args: ["install", "-g", "@profullstack/bufferoverride"] },
97
+ },
85
98
  secrets: {
86
99
  desc: "LogicSRC — end-to-end-encrypted team credential sharing (login, teams, credentials)",
87
100
  // The passthrough target is the `logicsrc` binary; the moshcode command is
@@ -297,6 +310,32 @@ export const TOOLS = {
297
310
  // place to hear it than an install that succeeded.
298
311
  install: { cmd: "npm", args: ["install", "-g", "@alchemy/cli"] },
299
312
  },
313
+ elevenlabs: {
314
+ desc: "ElevenLabs — build, configure and deploy Eleven Agents (plus voices, TTS, dubbing, and the rest of the API)",
315
+ bin: "elevenlabs",
316
+ // A workflow tool rather than an engine, despite the name. ElevenLabs calls
317
+ // these "agents", but they are conversational voice agents you configure and
318
+ // deploy to their platform: `elevenlabs agents push/pull/list` is an API
319
+ // client that runs one request and exits. `/agents` promises to hand the
320
+ // terminal to a live session, and there is no session here to hand it, so
321
+ // this belongs next to the other workflow CLIs. Docs:
322
+ // https://elevenlabs.io/docs/eleven-agents/operate/cli
323
+ //
324
+ // Authenticate once with `moshcode elevenlabs auth login` — a PKCE OAuth
325
+ // flow that stores the credential in the system keyring (falling back to a
326
+ // file under ~/.config), rather than an env var to keep exporting.
327
+ //
328
+ // The published package is a tiny Node shim; the real CLI is a native
329
+ // binary shipped per platform as an OPTIONAL dependency
330
+ // (@elevenlabs/cli-linux-x64 and friends). That is why the install spec is
331
+ // the plain global install with no flags: an install run with
332
+ // `--omit=optional` or `--no-optional` still succeeds and still puts
333
+ // `elevenlabs` on PATH, but every invocation then dies with "the platform
334
+ // package is not installed". `npm install -g` is idempotent and the CLI
335
+ // ships no updater of its own, so this doubles as the upgrade path and
336
+ // there is deliberately no `upgrade` key.
337
+ install: { cmd: "npm", args: ["install", "-g", "@elevenlabs/cli"] },
338
+ },
300
339
  };
301
340
 
302
341
  /** Resolve a name to `[key, tool]`, or null. */