@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 +10 -1
- package/dist/cli.js +97 -2
- package/dist/mcp-bridge.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# @ldgr/cli
|
|
2
2
|
|
|
3
|
-
LDGR's self-custody terminal wallet. Version 0.5.
|
|
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.
|
|
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(
|
|
1037
|
+
parsed = parseArguments(argv);
|
|
943
1038
|
} catch (error) {
|
|
944
1039
|
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
945
1040
|
`);
|