@aeon-ai-pay/aigateway 0.1.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/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/bin/cli.mjs +155 -0
- package/docs/env-vars.md +73 -0
- package/docs/exit-codes.md +65 -0
- package/docs/ide-setup.md +60 -0
- package/docs/output-schema.md +188 -0
- package/docs/recipes/cron-issue-cards.md +69 -0
- package/docs/recipes/error-recovery.md +53 -0
- package/docs/recipes/integrate-in-agent.md +108 -0
- package/docs/recipes/merchant-integration.md +243 -0
- package/docs/release-process.md +98 -0
- package/docs/troubleshooting.md +200 -0
- package/package.json +58 -0
- package/scripts/postinstall.mjs +40 -0
- package/skills/aigateway/SKILL.md +370 -0
- package/skills/aigateway/references/check-status.md +68 -0
- package/skills/aigateway/references/create-card.md +114 -0
- package/skills/aigateway/references/store.md +87 -0
- package/skills/aigateway/references/x402-protocol.md +143 -0
- package/src/balance.mjs +92 -0
- package/src/commands/clean.mjs +65 -0
- package/src/commands/create-card-status.mjs +67 -0
- package/src/commands/create-card.mjs +333 -0
- package/src/commands/create-image.mjs +428 -0
- package/src/commands/wallet-balance.mjs +47 -0
- package/src/commands/wallet-gas.mjs +99 -0
- package/src/commands/wallet-init.mjs +42 -0
- package/src/commands/wallet-topup.mjs +221 -0
- package/src/commands/wallet-withdraw.mjs +183 -0
- package/src/config.mjs +50 -0
- package/src/constants.mjs +22 -0
- package/src/error-codes.mjs +50 -0
- package/src/funding.mjs +216 -0
- package/src/output.mjs +85 -0
- package/src/sanitize.mjs +48 -0
- package/src/update-check.mjs +69 -0
- package/src/walletconnect.mjs +712 -0
- package/src/x402.mjs +120 -0
- package/templates/cline/.clinerules +53 -0
- package/templates/codex/AGENTS.md +56 -0
- package/templates/cursor/.cursor/rules/aigateway.mdc +60 -0
- package/templates/windsurf/.windsurfrules +48 -0
package/src/x402.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 协议客户端:初始化 EVM signer + x402Client
|
|
3
|
+
*/
|
|
4
|
+
import { x402Client, wrapAxiosWithPayment, x402HTTPClient } from "@aeon-ai-pay/axios";
|
|
5
|
+
import { registerExactEvmScheme } from "@aeon-ai-pay/evm/exact/client";
|
|
6
|
+
import { toClientEvmSigner } from "@aeon-ai-pay/evm";
|
|
7
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
8
|
+
import { createWalletClient, http, publicActions, formatUnits } from "viem";
|
|
9
|
+
import { bsc } from "viem/chains";
|
|
10
|
+
import { BSC_RPC_URL } from "./constants.mjs";
|
|
11
|
+
import axios from "axios";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 创建已注册 EVM 签名的 x402 axios 客户端
|
|
15
|
+
* @param {`0x${string}`} privateKey - EVM 私钥
|
|
16
|
+
* @returns {{ api: AxiosInstance, client: x402Client, address: string, getOrderNo: () => string|null }}
|
|
17
|
+
*/
|
|
18
|
+
export function createX402Api(privateKey) {
|
|
19
|
+
const evmAccount = privateKeyToAccount(privateKey);
|
|
20
|
+
const walletClient = createWalletClient({
|
|
21
|
+
account: evmAccount,
|
|
22
|
+
chain: bsc,
|
|
23
|
+
transport: http(BSC_RPC_URL),
|
|
24
|
+
}).extend(publicActions);
|
|
25
|
+
|
|
26
|
+
const evmSigner = toClientEvmSigner({
|
|
27
|
+
address: evmAccount.address,
|
|
28
|
+
signTypedData: (message) => evmAccount.signTypedData(message),
|
|
29
|
+
readContract: (args) =>
|
|
30
|
+
walletClient.readContract({ ...args, args: args.args || [] }),
|
|
31
|
+
sendTransaction: (args) =>
|
|
32
|
+
walletClient.sendTransaction({ to: args.to, data: args.data }),
|
|
33
|
+
waitForTransactionReceipt: (args) =>
|
|
34
|
+
walletClient.waitForTransactionReceipt(args),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const client = new x402Client();
|
|
38
|
+
registerExactEvmScheme(client, { signer: evmSigner });
|
|
39
|
+
|
|
40
|
+
const axiosInstance = axios.create();
|
|
41
|
+
|
|
42
|
+
// 在 wrapAxiosWithPayment 之前注册拦截器,
|
|
43
|
+
// 从 402 响应体中捕获 orderNo(服务端在 firstRequest 返回)
|
|
44
|
+
let capturedOrderNo = null;
|
|
45
|
+
axiosInstance.interceptors.response.use(
|
|
46
|
+
(response) => response,
|
|
47
|
+
(error) => {
|
|
48
|
+
if (error.response?.status === 402 && error.response?.data?.orderNo) {
|
|
49
|
+
capturedOrderNo = error.response.data.orderNo;
|
|
50
|
+
}
|
|
51
|
+
return Promise.reject(error);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const api = wrapAxiosWithPayment(axiosInstance, client);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
api,
|
|
59
|
+
client,
|
|
60
|
+
address: evmAccount.address,
|
|
61
|
+
getOrderNo: () => capturedOrderNo,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 第一次发起 x402 请求(不带签名),从 402 响应中提取实际付款要求。
|
|
67
|
+
* 同时保留完整的 402 响应数据和原始请求配置,供后续手动签名使用。
|
|
68
|
+
* 字段名与 x402 v2 PaymentRequirements 标准对齐:asset、payTo、amount。
|
|
69
|
+
*
|
|
70
|
+
* 兼容 GET(card 路径)和 POST(image / Skill Boss 路径)。
|
|
71
|
+
*
|
|
72
|
+
* @param {string} url
|
|
73
|
+
* @param {{ method?: "GET"|"POST", data?: any, headers?: object }} [options]
|
|
74
|
+
* @returns {Promise<{amountUsdt: number, amountWei: string, decimals: number, asset: string, payTo: string, orderNo: string|null, raw402Response: object, requestConfig: object}>}
|
|
75
|
+
*/
|
|
76
|
+
export async function fetchPaymentRequirements(url, options = {}) {
|
|
77
|
+
const rawClient = axios.create();
|
|
78
|
+
const method = (options.method || "GET").toUpperCase();
|
|
79
|
+
try {
|
|
80
|
+
if (method === "POST") {
|
|
81
|
+
await rawClient.post(url, options.data, { headers: options.headers });
|
|
82
|
+
} else {
|
|
83
|
+
await rawClient.get(url, { headers: options.headers });
|
|
84
|
+
}
|
|
85
|
+
throw new Error("Expected HTTP 402 but got 200");
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err.response?.status !== 402) throw err;
|
|
88
|
+
const data = err.response.data;
|
|
89
|
+
const accept = data?.accepts?.[0];
|
|
90
|
+
if (!accept) throw new Error("No payment requirements in 402 response");
|
|
91
|
+
const decimals = accept.tokenDecimals || 18;
|
|
92
|
+
const amountWei = BigInt(accept.amount);
|
|
93
|
+
const amountUsdt = parseFloat(formatUnits(amountWei, decimals));
|
|
94
|
+
return {
|
|
95
|
+
amountUsdt,
|
|
96
|
+
amountWei: amountWei.toString(),
|
|
97
|
+
decimals,
|
|
98
|
+
asset: accept.asset,
|
|
99
|
+
payTo: accept.payTo,
|
|
100
|
+
orderNo: data.orderNo || null,
|
|
101
|
+
raw402Response: err.response,
|
|
102
|
+
requestConfig: err.config,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 从响应头中解码 PAYMENT-RESPONSE(x402 v2)
|
|
109
|
+
* @param {object} headers - axios response headers
|
|
110
|
+
* @returns {object|null}
|
|
111
|
+
*/
|
|
112
|
+
export function decodePaymentResponse(headers) {
|
|
113
|
+
const raw = headers["payment-response"] || headers["PAYMENT-RESPONSE"];
|
|
114
|
+
if (!raw) return null;
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(Buffer.from(raw, "base64").toString("utf-8"));
|
|
117
|
+
} catch {
|
|
118
|
+
return { raw };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# aigateway — Virtual Card via x402
|
|
2
|
+
|
|
3
|
+
Trigger this rule when the user wants to create / query / manage one-time virtual debit cards funded via USDT on BSC.
|
|
4
|
+
|
|
5
|
+
## Setup (run once)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
aigateway wallet-init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`envelope.data.ready === true` means ready.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
aigateway create-card --amount <USD> --poll # Create card, poll until ready
|
|
17
|
+
aigateway create-card --amount <USD> --app-id <merchantId> --poll # With merchant app ID
|
|
18
|
+
aigateway create-card-status --order-no <orderNo> # Single status query
|
|
19
|
+
aigateway create-card-status --order-no <orderNo> --poll # Poll until terminal
|
|
20
|
+
aigateway wallet-balance # Show local wallet balance
|
|
21
|
+
aigateway wallet-topup --amount <USDT> # WalletConnect: USDT top-up
|
|
22
|
+
aigateway wallet-gas --amount <BNB> # WalletConnect: BNB gas top-up
|
|
23
|
+
aigateway wallet-withdraw [--to 0x...] [--amount <USDT>] # Reclaim funds
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Envelope
|
|
27
|
+
|
|
28
|
+
stdout = one line of JSON.
|
|
29
|
+
|
|
30
|
+
- Success: `{ "ok": true, "command": ..., "data": { ... } }`
|
|
31
|
+
- Failure: `{ "ok": false, "command": ..., "error": { "code": ..., "message": ..., ... } }`
|
|
32
|
+
|
|
33
|
+
Branch on `error.code`. Exit codes: `0`/`1`/`2`/`3`/`4` = success/user/timeout/service/internal.
|
|
34
|
+
|
|
35
|
+
## Hard Rules
|
|
36
|
+
|
|
37
|
+
- No private keys from the user.
|
|
38
|
+
- No full card numbers, CVV, or expiry in output (already sanitized).
|
|
39
|
+
- WalletConnect-opening commands (`create-card` / `create-image` when underfunded / `wallet-init` / `wallet-gas`) must run in foreground.
|
|
40
|
+
- No auto-retry on `PAYMENT_REJECTED` or `PAYMENT_TIMEOUT`.
|
|
41
|
+
|
|
42
|
+
## Recovery Cheatsheet
|
|
43
|
+
|
|
44
|
+
| Code | Action |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `AMOUNT_OUT_OF_RANGE` | Show `error.min`/`error.max`; ask user. |
|
|
47
|
+
| `INSUFFICIENT_USDT` | `aigateway wallet-init` then retry. |
|
|
48
|
+
| `INSUFFICIENT_BNB` | `aigateway wallet-gas` then retry. |
|
|
49
|
+
| `POLL_TIMEOUT` | Note `orderNo`; query later. |
|
|
50
|
+
| `PAYMENT_REJECTED` / `PAYMENT_TIMEOUT` | Ask user before retrying. |
|
|
51
|
+
| `SERVICE_UNAVAILABLE` | Exponential backoff retry. |
|
|
52
|
+
|
|
53
|
+
Full reference: `docs/output-schema.md`, `docs/exit-codes.md`, `docs/recipes/`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# aigateway — Virtual Card Agent Rules
|
|
2
|
+
|
|
3
|
+
When a user asks to **create, query, or manage** a one-time virtual debit card funded with USDT on BSC, use the `aigateway` CLI as the source of truth.
|
|
4
|
+
|
|
5
|
+
## Setup (run once per environment)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
aigateway wallet-init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Returns an envelope on stdout; `envelope.data.ready === true` means ready.
|
|
12
|
+
|
|
13
|
+
## Command Surface
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
aigateway create-card --amount <USD> [--app-id <merchantId>] --poll # Create card
|
|
17
|
+
aigateway create-card-status --order-no <orderNo> [--poll] # Query status
|
|
18
|
+
aigateway wallet-balance # Local balance
|
|
19
|
+
aigateway wallet-topup --amount <USDT> # USDT top-up via WalletConnect
|
|
20
|
+
aigateway wallet-gas --amount <BNB> # BNB top-up via WalletConnect
|
|
21
|
+
aigateway wallet-withdraw [--to 0x...] [--amount <USDT>] # Reclaim funds
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--app-id` defaults to `TEST000001`. `--quiet` suppresses stderr noise. `--legacy-output` swaps the envelope for the old pre-envelope JSON shape.
|
|
25
|
+
|
|
26
|
+
## Output Contract
|
|
27
|
+
|
|
28
|
+
Every command emits **one line of JSON** to stdout (the envelope):
|
|
29
|
+
|
|
30
|
+
- `{ "ok": true, "command": "...", "data": { ... } }`
|
|
31
|
+
- `{ "ok": false, "command": "...", "error": { "code": "...", "message": "...", ... } }`
|
|
32
|
+
|
|
33
|
+
Stderr is human-readable progress. Match on `error.code` (stable), not on `error.message`.
|
|
34
|
+
|
|
35
|
+
Exit codes: `0` success · `1` user · `2` timeout · `3` service/network · `4` internal.
|
|
36
|
+
|
|
37
|
+
## Hard Rules
|
|
38
|
+
|
|
39
|
+
- **Never** prompt for private keys. The CLI auto-generates a local session wallet.
|
|
40
|
+
- **Never** display full card numbers, CVV, or expiry. The CLI already redacts these to `•••• 1234`.
|
|
41
|
+
- **Never** run `create-card` / `create-image` / `wallet-init` / `wallet-gas` in the background — they open a WalletConnect QR window.
|
|
42
|
+
- **Never** auto-retry `PAYMENT_REJECTED` or `PAYMENT_TIMEOUT`. Ask the user.
|
|
43
|
+
|
|
44
|
+
## Error Recovery (high-frequency cases)
|
|
45
|
+
|
|
46
|
+
| `error.code` | Action |
|
|
47
|
+
| ------------ | ------ |
|
|
48
|
+
| `AMOUNT_OUT_OF_RANGE` | Show `error.min` / `error.max`; re-prompt user. |
|
|
49
|
+
| `INSUFFICIENT_USDT` | Run `aigateway wallet-init`, then re-run create. |
|
|
50
|
+
| `INSUFFICIENT_BNB` | Run `aigateway wallet-gas`, then re-run the failing op. |
|
|
51
|
+
| `POLL_TIMEOUT` | Card may still be provisioning. Note `error.orderNo`. |
|
|
52
|
+
| `PAYMENT_REJECTED` | User cancelled. Ask before retrying. |
|
|
53
|
+
| `PAYMENT_TIMEOUT` | 5-minute approval window expired. Ask before retrying. |
|
|
54
|
+
| `SERVICE_UNAVAILABLE` | Retry with exponential backoff (1s → 4s → 16s, max 3). |
|
|
55
|
+
|
|
56
|
+
Full schema and recipes: see `docs/output-schema.md`, `docs/exit-codes.md`, and `docs/recipes/`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use the aigateway CLI to create and manage one-time virtual debit cards via the x402 payment protocol on BSC. Trigger on intents like "create a card", "buy a virtual card", "card status", "top up", "withdraw funds".
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# aigateway — Virtual Card via x402
|
|
7
|
+
|
|
8
|
+
Use this rule when the user wants to **create, query, or manage** one-time-use virtual debit cards funded via USDT on BSC.
|
|
9
|
+
|
|
10
|
+
## First-time setup (always run once)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
aigateway wallet-init
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Auto-creates a local session wallet if missing. Returns an envelope; `envelope.data.ready === true` means ready.
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
| Intent | Command |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| Create a $N card and poll until ready | `aigateway create-card --amount <N> --poll` |
|
|
23
|
+
| Specify merchant app ID (optional) | `aigateway create-card --amount <N> --app-id <merchantId> --poll` |
|
|
24
|
+
| Check status of an existing order | `aigateway create-card-status --order-no <orderNo>` |
|
|
25
|
+
| Show local wallet balance | `aigateway wallet-balance` |
|
|
26
|
+
| Top up USDT from main wallet (interactive WalletConnect) | `aigateway wallet-topup --amount <USDT>` |
|
|
27
|
+
| Top up BNB for gas (interactive WalletConnect) | `aigateway wallet-gas --amount <BNB>` |
|
|
28
|
+
| Withdraw funds back to main wallet | `aigateway wallet-withdraw [--to 0x...] [--amount <USDT>]` |
|
|
29
|
+
|
|
30
|
+
## Output Contract
|
|
31
|
+
|
|
32
|
+
Every command writes **one line of JSON** to stdout: the envelope.
|
|
33
|
+
|
|
34
|
+
- Success: `{ "ok": true, "command": "...", "data": { ... } }`
|
|
35
|
+
- Failure: `{ "ok": false, "command": "...", "error": { "code": "...", "message": "...", ... } }`
|
|
36
|
+
|
|
37
|
+
Stderr is human-readable progress; pass `--quiet` if you want it suppressed.
|
|
38
|
+
|
|
39
|
+
## Error Handling
|
|
40
|
+
|
|
41
|
+
Branch on `error.code` (stable). Common cases:
|
|
42
|
+
|
|
43
|
+
- `AMOUNT_OUT_OF_RANGE` — Show valid range from `error.min` / `error.max`, ask user.
|
|
44
|
+
- `INSUFFICIENT_USDT` / `INSUFFICIENT_BNB` — Run `aigateway wallet-init` / `aigateway wallet-gas` then retry.
|
|
45
|
+
- `PAYMENT_REJECTED` / `PAYMENT_TIMEOUT` — User cancelled or didn't respond. **Do not auto-retry**; ask first.
|
|
46
|
+
- `POLL_TIMEOUT` — Card may still be provisioning. Note the `orderNo`, query later.
|
|
47
|
+
- Exit codes: `0` success, `1` user error, `2` timeout, `3` service/network, `4` internal.
|
|
48
|
+
|
|
49
|
+
## Hard Rules
|
|
50
|
+
|
|
51
|
+
- **Never** ask the user for a private key — the local wallet is auto-generated.
|
|
52
|
+
- **Never** display the full card number, CVV, or expiry. Output already redacts these to `•••• 1234`.
|
|
53
|
+
- **Never** run `create-card` / `create-image` / `wallet-init` / `wallet-gas` in the background — they may open a WalletConnect QR window that needs user attention.
|
|
54
|
+
- **Never** auto-retry rejected / timed-out signatures.
|
|
55
|
+
|
|
56
|
+
## Full Reference
|
|
57
|
+
|
|
58
|
+
- Envelope schema: `docs/output-schema.md`
|
|
59
|
+
- Exit / error codes: `docs/exit-codes.md`
|
|
60
|
+
- Integration recipes: `docs/recipes/`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# aigateway — Virtual Card via x402
|
|
2
|
+
|
|
3
|
+
When the user wants to create / query / manage a one-time virtual debit card funded with USDT on BSC, use the `aigateway` CLI.
|
|
4
|
+
|
|
5
|
+
## First-time setup (always run once)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
aigateway wallet-init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`envelope.data.ready === true` means ready.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
| Intent | Command |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| Create a $N card and poll until ready | `aigateway create-card --amount <N> --poll` |
|
|
18
|
+
| Specify merchant app ID | `aigateway create-card --amount <N> --app-id <merchantId> --poll` |
|
|
19
|
+
| Check status of an existing order | `aigateway create-card-status --order-no <orderNo>` |
|
|
20
|
+
| Show local wallet balance | `aigateway wallet-balance` |
|
|
21
|
+
| Top up USDT (interactive WalletConnect) | `aigateway wallet-topup --amount <USDT>` |
|
|
22
|
+
| Top up BNB for gas (interactive WalletConnect) | `aigateway wallet-gas --amount <BNB>` |
|
|
23
|
+
| Withdraw funds back to main wallet | `aigateway wallet-withdraw [--to 0x...] [--amount <USDT>]` |
|
|
24
|
+
|
|
25
|
+
## Output Contract
|
|
26
|
+
|
|
27
|
+
Every command emits one line of JSON to stdout (the envelope):
|
|
28
|
+
|
|
29
|
+
- Success: `{ "ok": true, "command": "...", "data": { ... } }`
|
|
30
|
+
- Failure: `{ "ok": false, "command": "...", "error": { "code": "...", "message": "...", ... } }`
|
|
31
|
+
|
|
32
|
+
Branch on `error.code` (stable). Exit codes: `0` success, `1` user, `2` timeout, `3` service/network, `4` internal.
|
|
33
|
+
|
|
34
|
+
## Hard Rules
|
|
35
|
+
|
|
36
|
+
- Never ask for a private key. The local wallet is auto-generated.
|
|
37
|
+
- Never display full card numbers, CVV, or expiry — output is already sanitized.
|
|
38
|
+
- Never run `create-card` / `create-image` / `wallet-init` / `wallet-gas` in the background; they open a QR window requiring user attention.
|
|
39
|
+
- Never auto-retry `PAYMENT_REJECTED` / `PAYMENT_TIMEOUT`.
|
|
40
|
+
|
|
41
|
+
## Common Recovery
|
|
42
|
+
|
|
43
|
+
- `AMOUNT_OUT_OF_RANGE` → Show `error.min` / `error.max`, ask for a new amount.
|
|
44
|
+
- `INSUFFICIENT_USDT` → Run `aigateway wallet-init`, then retry create.
|
|
45
|
+
- `INSUFFICIENT_BNB` → Run `aigateway wallet-gas`, then retry the failing op.
|
|
46
|
+
- `POLL_TIMEOUT` → Note `orderNo`, ask user to query later with `aigateway create-card-status --order-no <orderNo>`.
|
|
47
|
+
|
|
48
|
+
Full reference: `docs/output-schema.md`, `docs/exit-codes.md`, `docs/recipes/`.
|