@ldgr/cli 0.5.0 → 0.5.1

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
@@ -1,6 +1,15 @@
1
1
  # @ldgr/cli
2
2
 
3
- LDGR's self-custody terminal wallet. Version 0.5.0 creates or imports local wallets, lets you select between them, shows addresses and balances, sends native Ledra or a token, and persistently switches the selected wallet between mainnet and testnet. It also reviews prepared MCP requests for transfers, token creation and escrow creation in a separate interactive terminal.
3
+ LDGR's self-custody terminal wallet. Version 0.5.1 creates or imports local wallets, lets you select between them, shows addresses and balances, sends native Ledra or a token, and persistently switches the selected wallet between mainnet and testnet. It also reviews prepared MCP requests for transfers, token creation and escrow creation in a separate interactive terminal.
4
+
5
+ The MCP launcher uses `ldgr mcp --stdin` with a UTF-8 JSON envelope for
6
+ `wallet-info` and `save-request`. Envelopes are limited to 128 KiB. Public wallet
7
+ metadata or the saved request path and approval arguments are returned as JSON;
8
+ failures return a redacted error code and exit status 1. These machine operations
9
+ do not migrate or unlock wallets. `ldgr mcp <base64url-envelope>` also supports an
10
+ allowlisted `command` envelope for interactive wallet setup and approval in the
11
+ user's terminal. Recovery-key values and passphrases must never be included in
12
+ an envelope; interactive secrets remain local terminal prompts.
4
13
 
5
14
  ```bash
6
15
  npm install -g @ldgr/cli
package/dist/cli.js CHANGED
@@ -644,9 +644,97 @@ async function approveMcpRequest(requestPath, walletPath, options = {}) {
644
644
  }
645
645
  }
646
646
 
647
+ // src/mcp-bridge.ts
648
+ var MAX_BYTES = 128 * 1024;
649
+ function commandArgs(input) {
650
+ if (!Array.isArray(input) || input.some((value) => typeof value !== "string" || !value || /[\x00-\x1f\x7f-\x9f\p{Cf}\u2028\u2029]/u.test(value)))
651
+ throw new TypeError("Invalid arguments");
652
+ const positional = [];
653
+ let testnet = false, wallet = false;
654
+ for (let index = 0;index < input.length; index++) {
655
+ const value = input[index];
656
+ if (value === "--wallet") {
657
+ if (wallet || !input[index + 1] || input[index + 1].startsWith("-"))
658
+ throw new TypeError("Invalid wallet path");
659
+ wallet = true;
660
+ index++;
661
+ } else if (value === "--testnet") {
662
+ if (testnet)
663
+ throw new TypeError("Repeated network flag");
664
+ testnet = true;
665
+ } else if (value.startsWith("-"))
666
+ throw new TypeError("Unsupported option");
667
+ else
668
+ positional.push(value);
669
+ }
670
+ const [first, second, third] = positional;
671
+ const creation = first === "wallet" && (second === "create" || second === "import");
672
+ if (testnet && !creation)
673
+ throw new TypeError("Invalid network flag");
674
+ const valid = first === "wallet" && second === "create" && positional.length === 2 || first === "wallet" && second === "import" && (positional.length === 2 || positional.length === 3) || first === "wallet" && second === "list" && positional.length === 2 || first === "wallet" && second === "select" && positional.length === 3 && /^[0-9]+$/.test(third) || first === "approve" && positional.length === 2 || first === "address" && positional.length === 1 || first === "network" && second === "show" && positional.length === 2 || first === "network" && second === "set" && positional.length === 3 && (third === "mainnet" || third === "testnet");
675
+ if (!valid)
676
+ throw new TypeError("Unsupported command");
677
+ return [...input];
678
+ }
679
+ function envelope(bytes, machineOnly) {
680
+ if (bytes.length > MAX_BYTES)
681
+ throw new TypeError("Envelope too large");
682
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
683
+ if (!value || typeof value !== "object" || Array.isArray(value))
684
+ throw new TypeError("Invalid envelope");
685
+ const keys = value.operation === "wallet-info" ? ["operation", "walletPath"] : value.operation === "save-request" ? ["operation", "walletPath", "directory", "request"] : value.operation === "command" && !machineOnly ? ["operation", "args"] : [];
686
+ if (!keys.length || Object.keys(value).some((key) => !keys.includes(key)))
687
+ throw new TypeError("Unsupported envelope");
688
+ for (const key of ["walletPath", "directory"]) {
689
+ if (key in value && (typeof value[key] !== "string" || !value[key].trim() || /[\x00-\x1f\x7f-\x9f\p{Cf}\u2028\u2029]/u.test(value[key])))
690
+ throw new TypeError("Invalid path");
691
+ }
692
+ if (value.operation === "command")
693
+ value.args = commandArgs(value.args);
694
+ return value;
695
+ }
696
+ async function readEnvelope(argument) {
697
+ if (argument === "--stdin") {
698
+ const chunks = [];
699
+ let size = 0;
700
+ for await (const chunk of process.stdin) {
701
+ const bytes = Buffer.from(chunk);
702
+ size += bytes.length;
703
+ if (size > MAX_BYTES)
704
+ throw new TypeError("Envelope too large");
705
+ chunks.push(bytes);
706
+ }
707
+ return envelope(Buffer.concat(chunks), true);
708
+ }
709
+ if (argument.length > Math.ceil(MAX_BYTES * 4 / 3) || !/^[A-Za-z0-9_-]+$/.test(argument))
710
+ throw new TypeError("Invalid encoding");
711
+ const bytes = Buffer.from(argument, "base64url");
712
+ if (bytes.toString("base64url") !== argument)
713
+ throw new TypeError("Invalid encoding");
714
+ return envelope(bytes, false);
715
+ }
716
+ async function runMcpBridge(args) {
717
+ try {
718
+ if (args.length !== 1)
719
+ throw new TypeError("Expected one envelope");
720
+ const request = await readEnvelope(args[0]);
721
+ if (request.operation === "command")
722
+ return request.args;
723
+ const result = request.operation === "wallet-info" ? await getMcpWalletInfo(request.walletPath) : await saveMcpRequest(validateMcpRequest(request.request), { walletPath: request.walletPath, directory: request.directory });
724
+ process.stdout.write(JSON.stringify(result) + `
725
+ `);
726
+ } catch (error) {
727
+ const code = error instanceof TypeError && error.message === "Request network does not match the selected CLI wallet" ? "WALLET_NETWORK_MISMATCH" : error instanceof Error && error.message.startsWith("Wallet file not found: ") ? "WALLET_NOT_FOUND" : "OPERATION_FAILED";
728
+ process.stdout.write(JSON.stringify({ error: { code } }) + `
729
+ `);
730
+ process.exitCode = 1;
731
+ }
732
+ return;
733
+ }
734
+
647
735
  // src/cli.ts
648
736
  var HELP = `
649
- LDGR CLI 0.5.0 — self-custody wallet
737
+ LDGR CLI 0.5.1 — self-custody wallet
650
738
 
651
739
  Usage:
652
740
  ldgr wallet create [--testnet] [--wallet <path>]
@@ -937,9 +1025,16 @@ ${file.address}`, "Selected network");
937
1025
  p2.outro(`${target} address: ${switched.address}`);
938
1026
  }
939
1027
  async function main() {
1028
+ let argv = process.argv.slice(2);
1029
+ if (argv[0] === "mcp") {
1030
+ const command = await runMcpBridge(argv.slice(1));
1031
+ if (!command)
1032
+ return;
1033
+ argv = command;
1034
+ }
940
1035
  let parsed;
941
1036
  try {
942
- parsed = parseArguments(process.argv.slice(2));
1037
+ parsed = parseArguments(argv);
943
1038
  } catch (error) {
944
1039
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
945
1040
  `);
@@ -0,0 +1,2 @@
1
+ /** Machine operations do not migrate, unlock, sign, or emit terminal UI. */
2
+ export declare function runMcpBridge(args: readonly string[]): Promise<readonly string[] | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ldgr/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ldgr": "dist/cli.js"