Skip to content

Commit 046557e

Browse files
Docs, skill improvements 2026-05-03 (#68)
* WIP * fix totals schema type * Improve copy with dante * fix: formatting * fix: minor path fix * fix: correctly handle update notifier in TTY and non-TTY modes * fix: update package to correct * fix: formatting * fix: improve update notifier
1 parent 59a0fe1 commit 046557e

7 files changed

Lines changed: 262 additions & 109 deletions

File tree

README.md

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Link CLI
22

3-
Link CLI lets agents get secure, one-time-use payment credentials from a Link wallet — so they can complete purchases on your behalf without ever storing your real card details.
3+
Link CLI lets agents get secure, one-time-use payment credentials from a Link wallet to complete purchases on your behalf — without storing your real card details.
4+
5+
The CLI can produce one of two credential types:
6+
7+
- A virtual card (PAN) for use with a standard web checkout form. The issued card works anywhere, and is not restricted to Link-enabled sellers or sellers that use Stripe.
8+
- A [Shared Payment Token](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens) (SPT) when the seller accepts programmatic payments through [Machine Payment Protocols](https://mpp.dev) (MPP)
9+
10+
For now, this is only available to US Link accounts.
411

512
## Installation
613

@@ -14,11 +21,31 @@ Or run directly with `npx`:
1421
npx @stripe/link-cli
1522
```
1623

17-
You can install the skill via `npx skills add stripe/link-cli`.
24+
### Use with agents
25+
26+
Install the skill:
27+
28+
```bash
29+
npx skills add stripe/link-cli
30+
```
31+
32+
By default when called from an agent (non-TTY), all commands use `toon` output — a compact, LLM-friendly text format. All commands accept `--format [format]` for structured output. Other formats: `json`, `yaml`, `md`, `jsonl`.
33+
34+
List available commands:
35+
36+
```bash
37+
link-cli --llms-full
38+
```
39+
40+
Get a command's full schema with `--schema`. Example:
41+
42+
```bash
43+
link-cli spend-request create --schema
44+
```
1845

19-
### MCP Server
46+
#### MCP Server
2047

21-
Link CLI can also run as a local MCP server. Add the following to your MCP client config (`.mcp.json`, etc.)
48+
Link CLI can run as a local MCP server. Add the following to your MCP client config (`.mcp.json`, etc.)
2249

2350
```json
2451
{
@@ -33,6 +60,12 @@ Link CLI can also run as a local MCP server. Add the following to your MCP clien
3360

3461
## Quickstart
3562

63+
Run a guided onboarding and demo flow:
64+
65+
```bash
66+
link-cli onboard
67+
```
68+
3669
### Login
3770

3871
The `link-cli` requires a Link account. You can log in to your existing one or [register online](https://app.link.com).
@@ -41,19 +74,19 @@ The `link-cli` requires a Link account. You can log in to your existing one or [
4174
link-cli auth login
4275
```
4376

44-
You'll receive a verification URL and a short phrase. Visit the URL, log in to your Link account, and enter the phrase to approve the connection.
77+
You receive a verification URL and a short phrase. Visit the URL, log in to your Link account, and enter the phrase to approve the connection.
4578

4679
### List payment methods
4780

4881
```bash
4982
link-cli payment-methods list
5083
```
5184

52-
Returns the cards and bank accounts saved to your Link account. Use the `id` field as `payment_method_id` in the next step. If you have no payment methods, you can [add new ones in Link](https://app.link.com/wallet).
85+
Returns the cards and bank accounts saved to your Link account. Use the `id` field as `payment_method_id` in the next step. If you have no payment methods, [add new ones in Link](https://app.link.com/wallet).
5386

5487
### Create a spend request
5588

56-
To request a secure, one-time payment credential from your Link wallet, you create a spend request. You specify a payment method in your account, as well as some merchant details, line items, and amounts.
89+
Create a spend request with a payment method, merchant details, line items, and amounts:
5790

5891
```bash
5992
link-cli spend-request create \
@@ -67,9 +100,9 @@ link-cli spend-request create \
67100
--request-approval
68101
```
69102

70-
The `--request-approval` flag triggers a push notification (or email) to the user for approval, then polls until the request is approved or denied.
103+
The `--request-approval` flag triggers a push notification to the user for approval, then polls until the request is approved or denied.
71104

72-
Users can easily approve requests with the [Link app](https://link.com/download).
105+
Easily approve requests with the [Link app](https://link.com/download).
73106

74107
#### Line items and totals
75108

@@ -81,7 +114,7 @@ Users can easily approve requests with the [Link app](https://link.com/download)
81114
--line-item "name:Running Shoes,unit_amount:12000,quantity:1,description:Trail runners"
82115
```
83116

84-
**`--total` keys:** `type` (required), `display_text` (required), `amount` (required)
117+
**`--total` keys:** `type` (required; one of: `subtotal`, `tax`, `total`), `display_text` (required), `amount` (required)
85118

86119
```bash
87120
--total "type:subtotal,display_text:Subtotal,amount:12000" \
@@ -90,21 +123,21 @@ Users can easily approve requests with the [Link app](https://link.com/download)
90123

91124
#### Credential types
92125

93-
By default, a spend request provisions a virtual card. For merchants that support the [Machine Payments Protocol](https://mpp.dev) (HTTP 402) and the Stripe payment method, you can instead include `--credential-type "shared_payment_token"`.
126+
By default, a spend request provisions a virtual card. For merchants that support the [Machine Payments Protocol](https://mpp.dev) (HTTP 402) and the Stripe payment method, instead pass `--credential-type "shared_payment_token"`.
94127

95128
### Execute payment
96129

97130
The approved spend request includes a `card` object with `number`, `cvc`, `exp_month`, `exp_year`, `billing_address`, and `valid_until`. Enter these into the merchant's checkout form.
98131

99132
```bash
100-
link-cli spend-request retrieve lsrq_001 --format json
133+
link-cli spend-request retrieve lsrq_001
101134
```
102-
By default, retrieving a spend request will not include card details. Use the `--include=card` to see unmasked card details.
135+
By default, retrieving a spend request doesn't include card details. Pass `--include card` to see unmasked card details.
103136

104137
For agent polling, pass `--interval` and optionally `--max-attempts`:
105138

106139
```bash
107-
link-cli spend-request retrieve lsrq_001 --interval 2 --max-attempts 150 --format json
140+
link-cli spend-request retrieve lsrq_001 --interval 2 --max-attempts 150
108141
```
109142

110143
Polling exits successfully only after the request reaches a terminal status such as `approved`, `denied`, or `expired`. If polling reaches `--timeout` or exhausts `--max-attempts` while the request is still non-terminal, the command exits non-zero with `code: "POLLING_TIMEOUT"` so callers do not treat a still-pending request as complete.
@@ -115,23 +148,22 @@ If the merchant supports MPP, use `link-cli mpp pay` instead:
115148
link-cli mpp pay https://climate.stripe.dev/api/contribute \
116149
--spend-request-id lsrq_001 \
117150
--method POST \
118-
--data '{"amount":100}' \
119-
--format json
151+
--data '{"amount":100}'
120152
```
121153

122154
## Advanced
123155

124156
### Authentication
125157

126158
```bash
127-
link-cli auth login --client-name "Claude Code" --format json # identify the connecting agent
128-
link-cli auth status --format json # check auth status
129-
link-cli auth logout --format json # disconnect
159+
link-cli auth login --client-name "Claude Code" # identify the connecting agent
160+
link-cli auth status # check auth status
161+
link-cli auth logout # disconnect
130162
```
131163

132-
When `--client-name` is provided, the name is shown in the Link app when the user approves the connection — e.g. `Claude Code on my-macbook` instead of `link-cli on my-macbook`.
164+
When you provide `--client-name`, the Link app displays it when you approve the connection — for example, `Claude Code on my-macbook` instead of `link-cli on my-macbook`.
133165

134-
`auth status --format json` includes an `update` field when a newer version is available:
166+
`auth status` includes an `update` field when a newer version is available:
135167

136168
```json
137169
{
@@ -144,7 +176,7 @@ When `--client-name` is provided, the name is shown in the Link app when the use
144176
}
145177
```
146178

147-
Set `NO_UPDATE_NOTIFIER=1` to suppress update checks (e.g. in CI).
179+
Set `NO_UPDATE_NOTIFIER=1` to suppress update checks (for example, in CI).
148180

149181
### Spend request lifecycle
150182

@@ -153,44 +185,37 @@ A spend request moves through: **create** → **request approval** → **approve
153185
**Required fields for create:** `payment_method_id`, `merchant_name`, `merchant_url`, `context`, `amount`
154186

155187
**Constraints:** `context` must be at least 100 characters; `amount` must not exceed 50000 (cents); `currency` must be a 3-letter ISO code.
156-
**Test mode:** Pass `--test` to create testmode credentials (uses test card `4242424242424242`). Useful for development and integration testing without using real payment methods.
188+
**Test mode:** Pass `--test` to create testmode credentials (uses test card `4242424242424242`), useful for development and integration testing without real payment methods.
157189

158190
```bash
159191
# Update before approval
160192
link-cli spend-request update lsrq_001 \
161-
--merchant-url https://press.stripe.com/working-in-public \
162-
--format json
193+
--merchant-url https://press.stripe.com/working-in-public
163194

164195
# Request approval separately (alternative to create --request-approval)
165-
link-cli spend-request request-approval lsrq_001 --format json
196+
link-cli spend-request request-approval lsrq_001
166197

167-
# Retrieve at any time (includes card credentials once approved)
168-
link-cli spend-request retrieve lsrq_001 --format json
198+
# Retrieve at any time (includes card credentials after approval)
199+
link-cli spend-request retrieve lsrq_001
169200
```
170201

171-
### Output formats
172-
173-
All commands accept `--format json` for structured JSON output. Other formats: `yaml`, `md`, `jsonl`, `toon` (default). Errors are returned as JSON with `code` and `message` fields, with exit code 1.
174-
175202
### MPP
176203

177-
Use `mpp pay` to complete purchases on merchants that use the [Machine Payments Protocol](https://mpp.dev). The spend request must use `credential_type: "shared_payment_token"` and be approved. The SPT is one-time-use — if payment fails, create a new spend request.
204+
Use `mpp pay` to complete purchases on merchants that use the [Machine Payments Protocol](https://mpp.dev). The spend request must use `credential_type: "shared_payment_token"` and you must approve it before paying. The SPT is one-time-use — if payment fails, create a new spend request.
178205

179206
```bash
180207
link-cli mpp pay https://climate.stripe.dev/api/contribute \
181208
--spend-request-id lsrq_001 \
182209
--method POST \
183210
--data '{"amount":100}' \
184-
--header "X-Custom: value" \
185-
--format json
211+
--header "X-Custom: value"
186212
```
187213

188214
Use `mpp decode` to validate a raw `WWW-Authenticate` header and extract the `network_id` needed for `shared_payment_token` spend requests:
189215

190216
```bash
191217
link-cli mpp decode \
192-
--challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."' \
193-
--format json
218+
--challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."'
194219
```
195220

196221
### Environment variables
@@ -203,7 +228,7 @@ link-cli mpp decode \
203228

204229
## Onboard
205230

206-
Run the guided setup flow — authenticates, checks payment methods, shows the app download QR, and walks through both demo flows:
231+
Run the guided setup flow — authenticates, checks payment methods, shows the app download QR, and runs both demo flows:
207232

208233
```bash
209234
link-cli onboard

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"lint": "biome lint .",
1111
"format": "biome format --write .",
1212
"check": "biome check --write .",
13-
"link-cli": "node packages/cli/dist/cli.js"
13+
"link-cli": "./packages/cli/dist/cli.js"
1414
},
1515
"devDependencies": {
1616
"@biomejs/biome": "^1.9.4",

packages/cli/src/cli.tsx

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { Cli } from 'incur';
2-
import updateNotifier from 'update-notifier';
32
import { createAuthCli } from './commands/auth';
43
import { createDemoCli } from './commands/demo';
54
import { createMppCli } from './commands/mpp';
65
import { createOnboardCli } from './commands/onboard';
76
import { createPaymentMethodsCli } from './commands/payment-methods';
87
import { createSpendRequestCli } from './commands/spend-request';
98
import { ResourceFactory } from './utils/resource-factory';
9+
import {
10+
createAgentUpdateInfoProvider,
11+
createInteractiveUpdateInfoProvider,
12+
renderInteractiveUpdateNotice,
13+
} from './utils/update-info';
1014

1115
declare const __CLI_VERSION__: string;
1216
declare const __BUILD_NUMBER__: string;
@@ -25,17 +29,29 @@ const factory = new ResourceFactory({ verbose, defaultHeaders });
2529
const authRepo = factory.createAuthResource();
2630
const spendRequestRepo = factory.createSpendRequestResource();
2731

28-
const notifier = updateNotifier({
29-
pkg: { name: cliName, version: cliVersion },
30-
});
31-
3232
const cli = Cli.create('link-cli', {
3333
description:
3434
'Create a secure, one-time payment credential from a Link wallet to let agents complete purchases on behalf of users.',
3535
version: `${cliVersion} (build ${buildNumber})`,
3636
});
3737

38-
cli.command(createAuthCli(authRepo, notifier.update));
38+
const isAgent =
39+
process.argv.includes('--format') || process.argv.includes('--mcp');
40+
const agentUpdateInfoProvider = createAgentUpdateInfoProvider(
41+
cliName,
42+
cliVersion,
43+
);
44+
let getUpdateInfo = agentUpdateInfoProvider;
45+
46+
if (!isAgent && process.stdout.isTTY) {
47+
const updateInfo = await agentUpdateInfoProvider({ polling: false });
48+
getUpdateInfo = createInteractiveUpdateInfoProvider(updateInfo);
49+
if (updateInfo) {
50+
process.stderr.write(renderInteractiveUpdateNotice(updateInfo));
51+
}
52+
}
53+
54+
cli.command(createAuthCli(authRepo, getUpdateInfo));
3955
cli.command(createSpendRequestCli(spendRequestRepo));
4056
cli.command(
4157
createPaymentMethodsCli(() => factory.createPaymentMethodsResource()),
@@ -52,12 +68,6 @@ cli.command(
5268
),
5369
);
5470

55-
const isAgent =
56-
process.argv.includes('--format') || process.argv.includes('--mcp');
57-
if (!isAgent) {
58-
notifier.notify({ defer: false });
59-
}
60-
6171
cli.serve();
6272

6373
export default cli;

packages/cli/src/commands/auth/index.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { Cli } from 'incur';
33
import { render } from 'ink';
44
import React from 'react';
55
import type { IAuthResource } from '../../auth/types';
6+
import type { UpdateInfoProvider } from '../../utils/update-info';
67
import { Login } from './login';
78
import { Logout } from './logout';
89
import { loginOptions, statusOptions } from './schema';
910
import { AuthStatus } from './status';
1011

1112
export function createAuthCli(
1213
authResource: IAuthResource,
13-
updateInfo?: { current: string; latest: string },
14+
getUpdateInfo?: UpdateInfoProvider,
1415
) {
1516
const cli = Cli.create('auth', {
1617
description: 'Authentication commands',
@@ -103,6 +104,21 @@ export function createAuthCli(
103104
options: statusOptions,
104105
outputPolicy: 'agent-only' as const,
105106
async *run(c) {
107+
const opts = c.options;
108+
const interval = opts.interval;
109+
const maxAttempts = opts.maxAttempts;
110+
const deadline = Date.now() + opts.timeout * 1000;
111+
const updateInfo = await getUpdateInfo?.({
112+
polling: interval > 0,
113+
});
114+
const update = updateInfo
115+
? {
116+
current_version: updateInfo.current,
117+
latest_version: updateInfo.latest,
118+
update_command: 'npm install -g @stripe/link-cli',
119+
}
120+
: undefined;
121+
106122
if (!c.agent && !c.formatExplicit) {
107123
return new Promise((resolve) => {
108124
const { waitUntilExit } = render(
@@ -119,15 +135,12 @@ export function createAuthCli(
119135
}
120136
: {}),
121137
credentials_path: storage.getPath(),
138+
...(update && { update }),
122139
});
123140
});
124141
});
125142
}
126143

127-
const opts = c.options;
128-
const interval = opts.interval;
129-
const maxAttempts = opts.maxAttempts;
130-
const deadline = Date.now() + opts.timeout * 1000;
131144
let attempts = 0;
132145
let previousAttemptData: string | undefined;
133146

@@ -143,13 +156,6 @@ export function createAuthCli(
143156
}
144157

145158
const auth = storage.getAuth();
146-
const update = updateInfo
147-
? {
148-
current_version: updateInfo.current,
149-
latest_version: updateInfo.latest,
150-
update_command: 'npm install -g @stripe/link-cli',
151-
}
152-
: undefined;
153159
if (auth) {
154160
yield {
155161
authenticated: true,

packages/cli/src/commands/spend-request/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const createOptions = z.object({
4949
.array(z.union([z.string(), z.record(z.string(), z.unknown())]))
5050
.default([])
5151
.describe(
52-
'Total (repeatable, key:value format). Keys: type (required), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"',
52+
'Total (repeatable, key:value format). Keys: type (required; one of: subtotal, tax, total), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"',
5353
),
5454
requestApproval: z
5555
.boolean()
@@ -105,6 +105,6 @@ export const updateOptions = z.object({
105105
.array(z.union([z.string(), z.record(z.string(), z.unknown())]))
106106
.default([])
107107
.describe(
108-
'Total (repeatable, key:value format). Keys: type (required), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"',
108+
'Total (repeatable, key:value format). Keys: type (required; one of: subtotal, tax, total), display_text (required), amount (required). Example: "type:total,display_text:Total,amount:5000"',
109109
),
110110
});

0 commit comments

Comments
 (0)