@visa/cli 4.1.0-rc.19 → 4.1.0-rc.2
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/dist/cli.js +288 -351
- package/dist/mcp-server/index.js +140 -139
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +3 -5
- package/server.json +2 -2
- package/dist/checkout-engine/adapters/generic.d.ts +0 -19
- package/dist/checkout-engine/adapters/generic.js +0 -201
- package/dist/checkout-engine/adapters/index.d.ts +0 -7
- package/dist/checkout-engine/adapters/index.js +0 -17
- package/dist/checkout-engine/adapters/stripe-like.d.ts +0 -10
- package/dist/checkout-engine/adapters/stripe-like.js +0 -21
- package/dist/checkout-engine/browser-launch.d.ts +0 -46
- package/dist/checkout-engine/browser-launch.js +0 -81
- package/dist/checkout-engine/ceremony.d.ts +0 -64
- package/dist/checkout-engine/ceremony.js +0 -261
- package/dist/checkout-engine/cli-engine.d.ts +0 -60
- package/dist/checkout-engine/cli-engine.js +0 -242
- package/dist/checkout-engine/detect.d.ts +0 -61
- package/dist/checkout-engine/detect.js +0 -372
- package/dist/checkout-engine/evidence.d.ts +0 -22
- package/dist/checkout-engine/evidence.js +0 -59
- package/dist/checkout-engine/executor.d.ts +0 -147
- package/dist/checkout-engine/executor.js +0 -1187
- package/dist/checkout-engine/hosted-approval.d.ts +0 -78
- package/dist/checkout-engine/hosted-approval.js +0 -171
- package/dist/checkout-engine/index.d.ts +0 -3
- package/dist/checkout-engine/index.js +0 -5
- package/dist/checkout-engine/inline-target.d.ts +0 -13
- package/dist/checkout-engine/inline-target.js +0 -37
- package/dist/checkout-engine/instrument.d.ts +0 -54
- package/dist/checkout-engine/instrument.js +0 -83
- package/dist/checkout-engine/live-fill-approval.d.ts +0 -52
- package/dist/checkout-engine/live-fill-approval.js +0 -107
- package/dist/checkout-engine/mandate.d.ts +0 -25
- package/dist/checkout-engine/mandate.js +0 -100
- package/dist/checkout-engine/outcome.d.ts +0 -30
- package/dist/checkout-engine/outcome.js +0 -190
- package/dist/checkout-engine/owner-only-file.d.ts +0 -10
- package/dist/checkout-engine/owner-only-file.js +0 -22
- package/dist/checkout-engine/package.json +0 -3
- package/dist/checkout-engine/pay-args.d.ts +0 -14
- package/dist/checkout-engine/pay-args.js +0 -44
- package/dist/checkout-engine/pay.d.ts +0 -1
- package/dist/checkout-engine/pay.js +0 -13
- package/dist/checkout-engine/receipt.d.ts +0 -81
- package/dist/checkout-engine/receipt.js +0 -109
- package/dist/checkout-engine/repo-env.d.ts +0 -11
- package/dist/checkout-engine/repo-env.js +0 -23
- package/dist/checkout-engine/run-live-fill.d.ts +0 -1
- package/dist/checkout-engine/run-live-fill.js +0 -443
- package/dist/checkout-engine/types.d.ts +0 -26
- package/dist/checkout-engine/types.js +0 -2
- package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
- package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +0 -49
- package/dist/checkout-engine/vgs-gateway/server-mint-client.js +0 -150
- package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -141
- package/dist/checkout-engine/vgs-live-instrument.js +0 -252
- package/dist/checkout-engine/vic-confirmation.d.ts +0 -34
- package/dist/checkout-engine/vic-confirmation.js +0 -39
- package/dist/skills/pair-visa-agent/SKILL.md +0 -299
- package/dist/skills/pair-visa-agent/scripts/setup.mjs +0 -48
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Our fail-closed freshness bound, matching the runner's 15-minute checkout
|
|
3
|
-
* review window — NOT a claim about VGS's actual assurance TTL (unpublished).
|
|
4
|
-
* The only proven-working configuration mints the intent right after the
|
|
5
|
-
* ceremony; anything older is refused before an intent is created.
|
|
6
|
-
*/
|
|
7
|
-
export const PURCHASE_ASSURANCE_MAX_AGE_MS = 15 * 60 * 1000;
|
|
8
|
-
/** Tolerated forward clock skew on `mintedAt` before it is treated as invalid. */
|
|
9
|
-
const PURCHASE_ASSURANCE_MAX_SKEW_MS = 2 * 60 * 1000;
|
|
10
|
-
export function decimalToMinor(value) {
|
|
11
|
-
// JSON files routinely carry a number where the contract says decimal string
|
|
12
|
-
// ("transactionAmount": 5): refuse non-strings HERE so every caller surfaces
|
|
13
|
-
// its scoped invalid/mismatch remedy instead of a raw TypeError crash.
|
|
14
|
-
if (typeof value !== 'string')
|
|
15
|
-
return null;
|
|
16
|
-
if (!/^\d+(\.\d{1,2})?$/.test(value))
|
|
17
|
-
return null;
|
|
18
|
-
const [whole, fraction = ''] = value.split('.');
|
|
19
|
-
const minor = Number(whole) * 100 + Number(fraction.padEnd(2, '0'));
|
|
20
|
-
return Number.isSafeInteger(minor) ? minor : null;
|
|
21
|
-
}
|
|
22
|
-
function validateTarget(reference, ctx) {
|
|
23
|
-
if (typeof reference.merchantName !== 'string' ||
|
|
24
|
-
typeof reference.merchantCountryCode !== 'string' ||
|
|
25
|
-
!reference.merchantName.trim() ||
|
|
26
|
-
!/^[A-Z]{2}$/.test(reference.merchantCountryCode)) {
|
|
27
|
-
throw new Error('VGS checkout reference merchant name/country is invalid');
|
|
28
|
-
}
|
|
29
|
-
let referenceHost;
|
|
30
|
-
try {
|
|
31
|
-
referenceHost = new URL(reference.merchantUrl).hostname;
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
throw new Error('VGS checkout reference merchant URL is invalid');
|
|
35
|
-
}
|
|
36
|
-
if (referenceHost !== ctx.merchantHost) {
|
|
37
|
-
throw new Error(`VGS credential merchant mismatch: ${referenceHost} vs ${ctx.merchantHost}`);
|
|
38
|
-
}
|
|
39
|
-
const amountMinor = decimalToMinor(reference.transactionAmount);
|
|
40
|
-
if (amountMinor === null)
|
|
41
|
-
throw new Error('VGS checkout reference amount is invalid');
|
|
42
|
-
if (amountMinor !== ctx.amountMinor) {
|
|
43
|
-
throw new Error(`VGS credential amount mismatch: ${amountMinor} vs ${ctx.amountMinor} minor units`);
|
|
44
|
-
}
|
|
45
|
-
// String() so a JSON-number currency (840) mismatches with a readable
|
|
46
|
-
// message instead of crashing on .toUpperCase().
|
|
47
|
-
const currency = String(reference.transactionCurrencyCode).toUpperCase();
|
|
48
|
-
if (currency !== ctx.currency) {
|
|
49
|
-
throw new Error(`VGS credential currency mismatch: ${currency} vs ${ctx.currency}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function validateCliCredential(value) {
|
|
53
|
-
if (typeof value.tokenId !== 'string' || !value.tokenId.trim()) {
|
|
54
|
-
throw new Error('CLI agent credential requires tokenId');
|
|
55
|
-
}
|
|
56
|
-
// Deliberately no assuranceData requirement: the artifact's enrollment-time
|
|
57
|
-
// assurance is identity/enrollment material and is never read here (#5709).
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Refuse to mint an intent unless the assurance is fresh and its declared
|
|
61
|
-
* scope matches the checkout target exactly. `now` is injectable so the
|
|
62
|
-
* freshness rules are testable with a pinned clock (same pattern as
|
|
63
|
-
* validateCredential).
|
|
64
|
-
*/
|
|
65
|
-
export function validatePurchaseAssurance(value, target, now = new Date()) {
|
|
66
|
-
const remedy = 'run a fresh device-binding ceremony scoped to this exact merchant, amount, and ' +
|
|
67
|
-
'currency, and pass its output via --purchase-assurance-file (#5709)';
|
|
68
|
-
if (value == null || typeof value !== 'object') {
|
|
69
|
-
throw new Error(`purchase assurance is missing — ${remedy}`);
|
|
70
|
-
}
|
|
71
|
-
if (value.assuranceData == null) {
|
|
72
|
-
throw new Error(`purchase assurance requires assuranceData — ${remedy}`);
|
|
73
|
-
}
|
|
74
|
-
const mintedAtMs = typeof value.mintedAt === 'string' ? Date.parse(value.mintedAt) : NaN;
|
|
75
|
-
if (!Number.isFinite(mintedAtMs)) {
|
|
76
|
-
throw new Error(`purchase assurance requires an ISO 8601 mintedAt — ${remedy}`);
|
|
77
|
-
}
|
|
78
|
-
const age = now.getTime() - mintedAtMs;
|
|
79
|
-
if (age < -PURCHASE_ASSURANCE_MAX_SKEW_MS) {
|
|
80
|
-
throw new Error(`purchase assurance mintedAt is in the future — ${remedy}`);
|
|
81
|
-
}
|
|
82
|
-
if (age > PURCHASE_ASSURANCE_MAX_AGE_MS) {
|
|
83
|
-
throw new Error(`purchase assurance is stale (minted ${value.mintedAt}, limit ` +
|
|
84
|
-
`${PURCHASE_ASSURANCE_MAX_AGE_MS / 60000} minutes) — ${remedy}`);
|
|
85
|
-
}
|
|
86
|
-
let targetHost;
|
|
87
|
-
try {
|
|
88
|
-
targetHost = new URL(target.merchantUrl).hostname;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
throw new Error('VGS checkout reference merchant URL is invalid');
|
|
92
|
-
}
|
|
93
|
-
if (value.merchantHost !== targetHost) {
|
|
94
|
-
throw new Error(`purchase assurance merchant mismatch: ${value.merchantHost} vs ${targetHost} — ${remedy}`);
|
|
95
|
-
}
|
|
96
|
-
const assuranceMinor = decimalToMinor(value.transactionAmount);
|
|
97
|
-
const targetMinor = decimalToMinor(target.transactionAmount);
|
|
98
|
-
if (assuranceMinor === null || assuranceMinor !== targetMinor) {
|
|
99
|
-
throw new Error(`purchase assurance amount mismatch: ${value.transactionAmount} vs ` +
|
|
100
|
-
`${target.transactionAmount} — ${remedy}`);
|
|
101
|
-
}
|
|
102
|
-
if (typeof value.transactionCurrencyCode !== 'string' ||
|
|
103
|
-
value.transactionCurrencyCode.toUpperCase() !==
|
|
104
|
-
String(target.transactionCurrencyCode).toUpperCase()) {
|
|
105
|
-
throw new Error(`purchase assurance currency mismatch: ${value.transactionCurrencyCode} vs ` +
|
|
106
|
-
`${target.transactionCurrencyCode} — ${remedy}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// `now` is injectable so expiry rules are testable with a pinned clock.
|
|
110
|
-
export function validateCredential(value, now = new Date()) {
|
|
111
|
-
if (!/^\d{12,19}$/.test(value.networkToken)) {
|
|
112
|
-
throw new Error('VGS credential network token must contain 12-19 digits');
|
|
113
|
-
}
|
|
114
|
-
if (!/^\d{3,4}$/.test(value.cryptogramValue)) {
|
|
115
|
-
throw new Error('VGS credential short DAVV must contain 3-4 digits');
|
|
116
|
-
}
|
|
117
|
-
if (typeof value.cryptogramType !== 'string' || value.cryptogramType.toUpperCase() !== 'DAVV') {
|
|
118
|
-
throw new Error(`VGS credential type ${value.cryptogramType || '<missing>'} is not DAVV`);
|
|
119
|
-
}
|
|
120
|
-
if (!Number.isInteger(value.expMonth) || value.expMonth < 1 || value.expMonth > 12) {
|
|
121
|
-
throw new Error('VGS credential expiration month is invalid');
|
|
122
|
-
}
|
|
123
|
-
if (!Number.isInteger(value.expYear) ||
|
|
124
|
-
value.expYear < now.getFullYear() ||
|
|
125
|
-
(value.expYear === now.getFullYear() && value.expMonth < now.getMonth() + 1)) {
|
|
126
|
-
throw new Error('VGS credential is expired');
|
|
127
|
-
}
|
|
128
|
-
if (value.cryptogramExpiresAt !== undefined) {
|
|
129
|
-
const expiresAtMs = Date.parse(value.cryptogramExpiresAt);
|
|
130
|
-
if (!Number.isFinite(expiresAtMs))
|
|
131
|
-
throw new Error('VGS credential expiry is invalid');
|
|
132
|
-
if (expiresAtMs - now.getTime() < 60_000) {
|
|
133
|
-
throw new Error('VGS credential has less than 60 seconds of validity remaining');
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Mints the full DPAN + short DAVV only after the executor revalidates the
|
|
139
|
-
* reviewed merchant, amount, and currency. The payment credential remains in
|
|
140
|
-
* memory, is never returned to the caller, and can be requested only once.
|
|
141
|
-
*/
|
|
142
|
-
export class VgsLiveInstrument {
|
|
143
|
-
reference;
|
|
144
|
-
cardholderName;
|
|
145
|
-
fetchCredential;
|
|
146
|
-
kind = 'agentic-token';
|
|
147
|
-
used = false;
|
|
148
|
-
minted = null;
|
|
149
|
-
constructor(reference, cardholderName, fetchCredential) {
|
|
150
|
-
this.reference = reference;
|
|
151
|
-
this.cardholderName = cardholderName;
|
|
152
|
-
this.fetchCredential = fetchCredential;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Non-null once VGS has actually issued a credential (the intent is consumed
|
|
156
|
-
* from that moment, even if local validation later rejects the credential) —
|
|
157
|
-
* exactly the population a VIC confirmation can be owed for.
|
|
158
|
-
*/
|
|
159
|
-
confirmationTarget() {
|
|
160
|
-
return this.minted;
|
|
161
|
-
}
|
|
162
|
-
async getCredential(ctx) {
|
|
163
|
-
if (this.used)
|
|
164
|
-
throw new Error('VGS live credential instrument is single-use');
|
|
165
|
-
this.used = true;
|
|
166
|
-
validateTarget(this.reference, ctx);
|
|
167
|
-
if (!this.reference.tokenId || !this.reference.intentId) {
|
|
168
|
-
throw new Error('VGS checkout reference requires tokenId and intentId');
|
|
169
|
-
}
|
|
170
|
-
if (!this.cardholderName.trim())
|
|
171
|
-
throw new Error('cardholder name is required');
|
|
172
|
-
const { tokenId, intentId, ...transaction } = this.reference;
|
|
173
|
-
const value = await this.fetchCredential({ tokenId, intentId, transaction });
|
|
174
|
-
this.minted = { tokenId, intentId };
|
|
175
|
-
try {
|
|
176
|
-
validateCredential(value);
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
// VGS already issued a cryptogram for this intent even though we refuse
|
|
180
|
-
// to fill it; "correct inputs and retry" would silently reuse a consumed
|
|
181
|
-
// intent, so steer the operator to a fresh one.
|
|
182
|
-
throw new Error(`${err.message} — a credential was issued by VGS but rejected locally; obtain a fresh intent before retrying`);
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
pan: value.networkToken,
|
|
186
|
-
expMonth: value.expMonth,
|
|
187
|
-
expYear: value.expYear,
|
|
188
|
-
cvc: value.cryptogramValue,
|
|
189
|
-
cardholderName: this.cardholderName.trim(),
|
|
190
|
-
...(value.cryptogramExpiresAt ? { credentialExpiresAt: value.cryptogramExpiresAt } : {}),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Consumes #5614's claimed CLI enrollment artifact (for the tokenId) plus a
|
|
196
|
-
* FRESH purchase-scoped assurance, and creates a fresh, transaction-scoped VIC
|
|
197
|
-
* intent + credential after checkout approval. This is the production-shaped
|
|
198
|
-
* bridge; unlike VgsLiveInstrument it never needs a pre-created intent ID.
|
|
199
|
-
*
|
|
200
|
-
* The enrollment artifact's stored assuranceData is deliberately never sent:
|
|
201
|
-
* replaying it makes intent creation succeed (HTTP 201) while the cryptogram
|
|
202
|
-
* deterministically never completes — the #5709 dead end. Purchase
|
|
203
|
-
* authorization is the fresh, merchant+amount+currency-scoped assurance,
|
|
204
|
-
* validated against the checkout target BEFORE any intent is minted so a
|
|
205
|
-
* doomed intent is never created.
|
|
206
|
-
*/
|
|
207
|
-
export class VgsAssuranceInstrument {
|
|
208
|
-
enrollment;
|
|
209
|
-
purchase;
|
|
210
|
-
target;
|
|
211
|
-
cardholderName;
|
|
212
|
-
mintCredential;
|
|
213
|
-
kind = 'agentic-token';
|
|
214
|
-
used = false;
|
|
215
|
-
minted = null;
|
|
216
|
-
constructor(enrollment, purchase, target, cardholderName, mintCredential) {
|
|
217
|
-
this.enrollment = enrollment;
|
|
218
|
-
this.purchase = purchase;
|
|
219
|
-
this.target = target;
|
|
220
|
-
this.cardholderName = cardholderName;
|
|
221
|
-
this.mintCredential = mintCredential;
|
|
222
|
-
}
|
|
223
|
-
/** See VgsLiveInstrument.confirmationTarget — same contract. */
|
|
224
|
-
confirmationTarget() {
|
|
225
|
-
return this.minted;
|
|
226
|
-
}
|
|
227
|
-
async getCredential(ctx) {
|
|
228
|
-
if (this.used)
|
|
229
|
-
throw new Error('VGS assurance instrument is single-use');
|
|
230
|
-
this.used = true;
|
|
231
|
-
validateCliCredential(this.enrollment);
|
|
232
|
-
validateTarget(this.target, ctx);
|
|
233
|
-
validatePurchaseAssurance(this.purchase, this.target);
|
|
234
|
-
if (!this.cardholderName.trim())
|
|
235
|
-
throw new Error('cardholder name is required');
|
|
236
|
-
const { payment: value, intentId } = await this.mintCredential({
|
|
237
|
-
tokenId: this.enrollment.tokenId,
|
|
238
|
-
assuranceData: this.purchase.assuranceData,
|
|
239
|
-
transaction: this.target,
|
|
240
|
-
});
|
|
241
|
-
this.minted = { tokenId: this.enrollment.tokenId, intentId };
|
|
242
|
-
validateCredential(value);
|
|
243
|
-
return {
|
|
244
|
-
pan: value.networkToken,
|
|
245
|
-
expMonth: value.expMonth,
|
|
246
|
-
expYear: value.expYear,
|
|
247
|
-
cvc: value.cryptogramValue,
|
|
248
|
-
cardholderName: this.cardholderName.trim(),
|
|
249
|
-
...(value.cryptogramExpiresAt ? { credentialExpiresAt: value.cryptogramExpiresAt } : {}),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { CheckoutOutcome } from './executor.js';
|
|
2
|
-
import type { VicConfirmationTarget } from './vgs-live-instrument.js';
|
|
3
|
-
export type VicTransactionStatus = 'APPROVED' | 'DECLINED';
|
|
4
|
-
export type PostVicConfirmation = (input: {
|
|
5
|
-
tokenId: string;
|
|
6
|
-
intentId: string;
|
|
7
|
-
transactionStatus: VicTransactionStatus;
|
|
8
|
-
transactionType: 'PURCHASE';
|
|
9
|
-
transactionTimestamp: string;
|
|
10
|
-
transaction: {
|
|
11
|
-
transactionAmount: string;
|
|
12
|
-
transactionCurrencyCode: string;
|
|
13
|
-
};
|
|
14
|
-
}) => Promise<unknown>;
|
|
15
|
-
export type VicConfirmationReport = {
|
|
16
|
-
posted: true;
|
|
17
|
-
transactionStatus: VicTransactionStatus;
|
|
18
|
-
} | {
|
|
19
|
-
posted: false;
|
|
20
|
-
reason: string;
|
|
21
|
-
};
|
|
22
|
-
/** Definitive merchant answers map to a status; everything else maps to none. */
|
|
23
|
-
export declare function outcomeToTransactionStatus(outcome: CheckoutOutcome): VicTransactionStatus | null;
|
|
24
|
-
export declare function reportVicOutcome(input: {
|
|
25
|
-
target: VicConfirmationTarget | null;
|
|
26
|
-
outcome: CheckoutOutcome;
|
|
27
|
-
transaction: {
|
|
28
|
-
transactionAmount: string;
|
|
29
|
-
transactionCurrencyCode: string;
|
|
30
|
-
};
|
|
31
|
-
post: PostVicConfirmation;
|
|
32
|
-
/** Injectable for tests; defaults to now. */
|
|
33
|
-
timestamp?: Date;
|
|
34
|
-
}): Promise<VicConfirmationReport>;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/** Definitive merchant answers map to a status; everything else maps to none. */
|
|
2
|
-
export function outcomeToTransactionStatus(outcome) {
|
|
3
|
-
if (outcome === 'confirmed')
|
|
4
|
-
return 'APPROVED';
|
|
5
|
-
if (outcome === 'declined')
|
|
6
|
-
return 'DECLINED';
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
export async function reportVicOutcome(input) {
|
|
10
|
-
const transactionStatus = outcomeToTransactionStatus(input.outcome);
|
|
11
|
-
if (!transactionStatus) {
|
|
12
|
-
return {
|
|
13
|
-
posted: false,
|
|
14
|
-
reason: `outcome '${input.outcome}' is not a definitive merchant answer — ` +
|
|
15
|
-
'resolve it with the merchant, then post the confirmation manually',
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
if (!input.target) {
|
|
19
|
-
return { posted: false, reason: 'no VIC credential was minted in this run' };
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
await input.post({
|
|
23
|
-
tokenId: input.target.tokenId,
|
|
24
|
-
intentId: input.target.intentId,
|
|
25
|
-
transactionStatus,
|
|
26
|
-
transactionType: 'PURCHASE',
|
|
27
|
-
transactionTimestamp: (input.timestamp ?? new Date()).toISOString(),
|
|
28
|
-
transaction: input.transaction,
|
|
29
|
-
});
|
|
30
|
-
return { posted: true, transactionStatus };
|
|
31
|
-
}
|
|
32
|
-
catch (err) {
|
|
33
|
-
return {
|
|
34
|
-
posted: false,
|
|
35
|
-
reason: `confirmation POST failed: ${err.message} — the merchant outcome stands; ` +
|
|
36
|
-
'retry the confirmation for this intent out-of-band',
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pair-visa-agent
|
|
3
|
-
description: Pair a Visa CLI v4 agent identity to this device so the agent can pay on the user's behalf. You paste a link, the user enrolls on mobile web, and the credential auto-pairs back to this device — no code is ever typed back. Use when the user says "pair my agent", "enroll my Visa CLI", "connect my Visa wallet", "set up my agent identity", "get me set up to pay", "log in to Visa", or asks to connect/sign up their .visa account.
|
|
4
|
-
compatibility: Needs the `visa` CLI (`npm i -g @visa/cli@rc`) — run `node scripts/setup.mjs` to auto-install it if missing — plus network access to the Visa verify-web origin. Works in OpenClaw, Hermes, or any Agent Skills runtime.
|
|
5
|
-
allowed-tools: Bash(visa:*) Bash(visa-cli:*) Bash(node:*) Bash(npm:*) Bash(npx:*)
|
|
6
|
-
metadata:
|
|
7
|
-
author: visa
|
|
8
|
-
homepage: https://visacli.sh/agents
|
|
9
|
-
version: '0.5.0'
|
|
10
|
-
# OpenClaw-namespaced extension (agentskills.io keeps `metadata` free-form, so
|
|
11
|
-
# non-standard runtime config lives here — `user-invocable` is not a standard
|
|
12
|
-
# top-level field). OpenClaw auto-installs `install[]` when `requires.bins` are
|
|
13
|
-
# missing; other runtimes (Hermes, Claude Code, …) self-provision via the
|
|
14
|
-
# bundled `scripts/setup.mjs` (see compatibility + Getting set up). @rc pinned
|
|
15
|
-
# until 4.1.0 is promoted to latest.
|
|
16
|
-
openclaw:
|
|
17
|
-
user-invocable: true
|
|
18
|
-
emoji: '💳'
|
|
19
|
-
requires:
|
|
20
|
-
bins:
|
|
21
|
-
- visa
|
|
22
|
-
install:
|
|
23
|
-
- kind: node
|
|
24
|
-
package: '@visa/cli@rc'
|
|
25
|
-
bins:
|
|
26
|
-
- visa
|
|
27
|
-
- visa-cli
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
# Pair a Visa CLI v4 Agent Identity
|
|
31
|
-
|
|
32
|
-
Connects a Visa v4 identity (`.visa` name + delegated wallet + card) to THIS device
|
|
33
|
-
through a mobile-web enrollment link. This is the device-link flow: the credential
|
|
34
|
-
pairs back **automatically** to the terminal that started it. **No confirmation code is
|
|
35
|
-
ever typed back into you** — the code is an out-of-band check the human reads to confirm
|
|
36
|
-
the page they are on is the flow you started.
|
|
37
|
-
|
|
38
|
-
Driven through the local `visa` binary. **This skill is runtime-agnostic** — it works in
|
|
39
|
-
OpenClaw, Hermes, or any agent runtime that can run the `visa` CLI or mount its MCP
|
|
40
|
-
server (see Runtimes). Use whichever pairing surface your runtime exposes; they all wrap
|
|
41
|
-
the same two `visa` commands:
|
|
42
|
-
|
|
43
|
-
- **`pair_agent_start` / `pair_agent_poll`** — the OpenClaw plugin tools (parse JSON for you). Use these if present.
|
|
44
|
-
- **`enroll_agent`** — the `visa` MCP server tool, if the runtime has the server mounted.
|
|
45
|
-
- **`visa agent enroll` / `visa agent enroll-claim`** — the raw CLI, always available if `visa` is on PATH. The universal fallback.
|
|
46
|
-
|
|
47
|
-
Pick the first one available; if unsure, shell out to the raw CLI commands — every path
|
|
48
|
-
hits the same hand-off on this device.
|
|
49
|
-
|
|
50
|
-
## Getting this skill
|
|
51
|
-
|
|
52
|
-
The skill ships **inside the public `@visa/cli` npm package** — no git clone of the
|
|
53
|
-
(private) monorepo is required, so any agent box that can reach npm can install it:
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
npm install -g @visa/cli@rc # public npm — puts `visa` on PATH
|
|
57
|
-
visa agent skill # plants this skill into your runtime's skills dir
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`visa agent skill` auto-detects OpenClaw / Hermes / Claude Code (falling back to the
|
|
61
|
-
project-local `./.agents/skills`); pass `--runtime <name>` or `--dir <path>` to target
|
|
62
|
-
one explicitly, `--force` to overwrite, or `--print` to read it without writing. After
|
|
63
|
-
it lands, **reload/restart your agent** so the runtime registers the skill, then prompt
|
|
64
|
-
the agent — "set up my Visa agent to pay" — to run the pairing flow below. (Installing a
|
|
65
|
-
skill does not run it; the agent activates it on a matching prompt.)
|
|
66
|
-
|
|
67
|
-
Access is gated server-side by the employee allowlist + Turnkey, so shipping the playbook
|
|
68
|
-
over public npm exposes no capability — only allowlisted accounts can actually pair.
|
|
69
|
-
|
|
70
|
-
(OpenClaw users also get it auto-bundled with the `@visa/visa-cli-openclaw` plugin. The
|
|
71
|
-
`npx skills add` open-standard path applies once the skill is published to a public repo.)
|
|
72
|
-
|
|
73
|
-
## Getting set up (install → mount → pair → transact)
|
|
74
|
-
|
|
75
|
-
The whole flow, top to bottom:
|
|
76
|
-
|
|
77
|
-
1. **Install the CLI (prerelease)** — `npm install -g @visa/cli@rc`, **or** run the bundled
|
|
78
|
-
provisioner `node scripts/setup.mjs` (idempotent — installs `@visa/cli@rc` only if `visa`
|
|
79
|
-
is missing; this is how **Hermes / Claude Code / any runtime** self-provisions, and what
|
|
80
|
-
OpenClaw runs automatically via the manifest). You **must** use the `@rc` tag: the
|
|
81
|
-
`@latest` tag (4.0.x) predates the `visa agent` commands and reports "does not expose
|
|
82
|
-
agent enroll". This puts both `visa` and `visa-cli` on PATH and ships the bundled MCP
|
|
83
|
-
server (`@visa/cli/dist/mcp-server/index.js`). (Once 4.1.0 is promoted to `latest`, bare
|
|
84
|
-
`@visa/cli` will work.)
|
|
85
|
-
2. **Mount the MCP server** — one mount gives this agent the _entire_ v4 toolset:
|
|
86
|
-
- **OpenClaw:** installing the `@visa/visa-cli-openclaw` plugin **auto-mounts** the server
|
|
87
|
-
(its postinstall writes `mcp.servers["visa-cli"]` into `~/.openclaw/openclaw.json`).
|
|
88
|
-
Nothing to do by hand.
|
|
89
|
-
- **Hermes / anything else:** run `visa-cli install hermes` (or `visa-cli install <runtime>`).
|
|
90
|
-
This writes the server entry idempotently into the runtime's config (Hermes →
|
|
91
|
-
`~/.hermes/config.yaml` under `mcp_servers`). See Runtimes below for the exact shape.
|
|
92
|
-
- **No runtime integration?** The raw `visa agent …` CLI still works — the universal fallback.
|
|
93
|
-
3. **Pair** — run the Core flow below. This binds a `.visa` identity + delegated wallet + card
|
|
94
|
-
to this device.
|
|
95
|
-
4. **Transact** — once paired, use the mounted tools (see "What you can do once paired").
|
|
96
|
-
|
|
97
|
-
## Runtimes
|
|
98
|
-
|
|
99
|
-
One artifact, both runtimes. The `visa` CLI is the portable substrate; mount its MCP
|
|
100
|
-
server so the runtime exposes the tools (both use the same server entrypoint — replace
|
|
101
|
-
`<npm root -g>` with the output of `npm root -g`):
|
|
102
|
-
|
|
103
|
-
**OpenClaw** (`~/.openclaw/openclaw.json`):
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
{
|
|
107
|
-
"mcp": {
|
|
108
|
-
"servers": {
|
|
109
|
-
"visa-cli": {
|
|
110
|
-
"command": "node",
|
|
111
|
-
"args": ["<npm root -g>/@visa/cli/dist/mcp-server/index.js"]
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
**Hermes** (`~/.hermes/config.yaml`):
|
|
119
|
-
|
|
120
|
-
```yaml
|
|
121
|
-
mcp_servers:
|
|
122
|
-
visa-cli:
|
|
123
|
-
command: node
|
|
124
|
-
args: ['<npm root -g>/@visa/cli/dist/mcp-server/index.js']
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
No MCP server configured? The raw `visa agent …` CLI commands below work as long as `visa`
|
|
128
|
-
is on PATH. (In Hermes, `hermes claw migrate` also imports this skill from an existing
|
|
129
|
-
OpenClaw install into `~/.hermes/skills/`.) See `RUNTIMES.md` for the full plugin/config map.
|
|
130
|
-
|
|
131
|
-
## Running commands
|
|
132
|
-
|
|
133
|
-
- Every command supports `--format json`; the tools use it. On success JSON goes to
|
|
134
|
-
stdout; on failure a JSON object with `error` (and, for claim, a `status`) goes to
|
|
135
|
-
stdout with a non-zero exit code. **Always parse the JSON — never scrape prose.**
|
|
136
|
-
- `enroll-claim` exit codes are load-bearing: `0` = claimed (done), `3` = not ready yet
|
|
137
|
-
(poll again), `1` = terminal failure (stop, recover).
|
|
138
|
-
|
|
139
|
-
## Core flow
|
|
140
|
-
|
|
141
|
-
Copy this checklist and track progress:
|
|
142
|
-
|
|
143
|
-
- [ ] Step 1 — Start the hand-off (`pair_agent_start` / `enroll_agent` / `visa agent enroll --format json`)
|
|
144
|
-
- [ ] Step 2 — Present the link AND the confirmation code to the user
|
|
145
|
-
- [ ] Step 3 — Poll until paired (`pair_agent_poll` / `visa agent enroll-claim --format json`) — auto-pairs, no code entry
|
|
146
|
-
- [ ] Step 4 — Confirm the identity is bound to this device
|
|
147
|
-
|
|
148
|
-
Below, "start" and "poll" mean whichever surface your runtime exposes (see the top of
|
|
149
|
-
this skill). The returned fields and the poll semantics are identical across all of them.
|
|
150
|
-
|
|
151
|
-
### Step 1 — Start the hand-off
|
|
152
|
-
|
|
153
|
-
**CRITICAL:** Only ONE hand-off can be in flight per device; starting a new one replaces
|
|
154
|
-
any prior. If the user says they already started enrolling, go to Step 3 first.
|
|
155
|
-
|
|
156
|
-
Start the hand-off (`pair_agent_start`, or `visa agent enroll --format json`). It returns
|
|
157
|
-
`browserUrl`, `confirmCode`, `expiresAt`.
|
|
158
|
-
|
|
159
|
-
If it returns an error containing `verify-web URL not available in stable builds`, this is
|
|
160
|
-
a stable CLI build with the surface not yet public. **Do NOT proceed** — tell the user to
|
|
161
|
-
set `VISA_VERIFY_WEB_URL` (or use a preview/RC build), then stop.
|
|
162
|
-
|
|
163
|
-
If it returns an error containing `RC build requires access` (often suffixed `Run: visa-cli
|
|
164
|
-
setup`), this is an **older RC build** whose employee gate still fires on the pairing
|
|
165
|
-
bootstrap. **Do NOT run `visa-cli setup`** — that is the legacy v3 GitHub-OAuth path, it is
|
|
166
|
-
NOT how v4 pairing authenticates, and it dead-ends on the same gate. Recover by either
|
|
167
|
-
upgrading to a current RC (`npm install -g @visa/cli@rc` — recent RCs exempt `agent enroll`
|
|
168
|
-
/ `agent enroll-claim` from this gate) or, if the operator has the code, setting the
|
|
169
|
-
`VISA_RC_CODE` env var on the host. Then retry the hand-off. Never substitute `setup` for
|
|
170
|
-
pairing.
|
|
171
|
-
|
|
172
|
-
### Step 2 — Present the link AND the confirmation code
|
|
173
|
-
|
|
174
|
-
Show BOTH clearly. The user opens the link on their phone.
|
|
175
|
-
|
|
176
|
-
**Formatting rules — make it clean AND tappable in chat clients like Telegram:**
|
|
177
|
-
|
|
178
|
-
- **Bare, tappable link.** Put `browserUrl` on its own line as a plain URL — no backticks, no
|
|
179
|
-
code span, and do **not** wrap it as a Markdown `[label](url)` link. Telegram auto-linkifies
|
|
180
|
-
a bare URL (one tap opens the phone's browser); a code span is unclickable, and the long
|
|
181
|
-
pairing query string (`?cli=…&cliPk=…`, with underscores) breaks Telegram MarkdownV2 link
|
|
182
|
-
parsing. Add a short "👉 Tap to open on your phone" cue so it reads as an action.
|
|
183
|
-
- **Emphasize the confirm code.** Show `confirmCode` in **bold** or `monospace` — both are
|
|
184
|
-
Telegram-safe (mono needs no escaping). It's read/compared, never clicked.
|
|
185
|
-
- **Relative expiry.** "expires in about 15 minutes" — never the raw `expiresAt` ISO/UTC
|
|
186
|
-
timestamp (e.g. `2026-07-21T21:22:57Z`); a UTC time reads as noise.
|
|
187
|
-
- **Scannable layout.** Short lines, a blank line between blocks, one leading emoji per block.
|
|
188
|
-
- **Don't over-Markdown.** Telegram MarkdownV2 requires escaping `_ * [ ] ( ) ~ > # + - = | { } . !`,
|
|
189
|
-
so heavy formatting risks a broken render. A bold/mono code + a bare link + emojis is plenty —
|
|
190
|
-
keep the rest plain prose.
|
|
191
|
-
|
|
192
|
-
Use this shape:
|
|
193
|
-
|
|
194
|
-
> 💳 **Pair your Visa agent** — open this on your phone:
|
|
195
|
-
>
|
|
196
|
-
> 👉 <browserUrl — bare, on its own line>
|
|
197
|
-
>
|
|
198
|
-
> 🔐 Confirmation code: **<confirmCode>**
|
|
199
|
-
>
|
|
200
|
-
> On the final screen, check the code matches before you finish — only finish if it does. Never
|
|
201
|
-
> type this code back to me or share it with anyone; it pairs back here automatically once you
|
|
202
|
-
> finish. Expires in about 15 minutes.
|
|
203
|
-
|
|
204
|
-
**Do NOT** ask the user to read the code back. **Do NOT** accept a code as input. Pairing
|
|
205
|
-
is proven by a secret held on this device, not by anything the user types.
|
|
206
|
-
|
|
207
|
-
### Step 3 — Poll until paired (auto-pair)
|
|
208
|
-
|
|
209
|
-
Poll (`pair_agent_poll`, or `visa agent enroll-claim --format json`). With the raw CLI,
|
|
210
|
-
read the **exit code** (`0` claimed / `3` not-ready / `1` terminal); the plugin tool
|
|
211
|
-
returns the same as a `done`/`status` object. Then, based on the result:
|
|
212
|
-
|
|
213
|
-
- `done: true` / exit 0, `status: "claimed"` → paired. Go to Step 4.
|
|
214
|
-
- `done: false` / exit 3, `status: "not_ready"` → the user hasn't finished. Tell them
|
|
215
|
-
you're still waiting, then poll again. **Do not loop forever** — each poll already
|
|
216
|
-
waits ~9s; after a handful of polls, ask the user whether they've finished on their phone.
|
|
217
|
-
- `done: true`, `status: "not_ready"`, `likelyExpired: true` → the 15-minute window
|
|
218
|
-
expired. Go back to Step 1.
|
|
219
|
-
- `done: true`, `status: "confirm_mismatch"` → **STOP.** See Errors.
|
|
220
|
-
- `done: true`, `status: "no_pending"` → no hand-off in flight. Go back to Step 1.
|
|
221
|
-
|
|
222
|
-
### Step 4 — Confirm the identity is bound
|
|
223
|
-
|
|
224
|
-
On `claimed`, tell the user their `name` (e.g. `alec.visa`) is paired to this device. Read
|
|
225
|
-
`keyBound`:
|
|
226
|
-
|
|
227
|
-
- `keyBound: true` → the agent key was generated on THIS device; its private half never
|
|
228
|
-
left it. Highest assurance.
|
|
229
|
-
- `keyBound: false` → an existing agent was connected from a returning sign-in; no agent
|
|
230
|
-
key is held locally. Normal and expected for returning users — the credential is fully
|
|
231
|
-
valid.
|
|
232
|
-
|
|
233
|
-
## What you can do once paired
|
|
234
|
-
|
|
235
|
-
Pairing is the on-ramp. v4 is **non-custodial** — a Turnkey-delegated wallet bounded by
|
|
236
|
-
on-device keys and policies, **not** a stored credit line or a server-custodied card.
|
|
237
|
-
Describe it that way to the user. The mounted MCP server exposes:
|
|
238
|
-
|
|
239
|
-
- **x402 wallet spend (the core rail)** — `wallet_discover` (find payable x402 services by
|
|
240
|
-
outcome; free) → `wallet_probe` (preview a fresh x402 challenge; free) → `wallet_pay` /
|
|
241
|
-
`wallet_directory_pay` (a bounded payment settled directly from the delegated wallet over
|
|
242
|
-
x402, wallet→endpoint — never touching credits, cards, or server-side spend controls).
|
|
243
|
-
These `wallet_*` tools are **default-on in the supported build** (the v4 wallet runtime is
|
|
244
|
-
bundled); spend stays bounded by the on-device wallet policy. The same rail is on the CLI
|
|
245
|
-
as `visa find` / `inspect` / `pay`.
|
|
246
|
-
- **Visa catalog (curated tools)** — `discover_tools` → `execute_tool` (or `visa tools` /
|
|
247
|
-
`visa run`): enumerate the curated catalog and run a tool; paid ones show a preview and
|
|
248
|
-
settle inline. Distinct from the open x402 directory above.
|
|
249
|
-
- **Subway mesh** — `subway_*` route calls across the agent mesh. **Gated:** present but only
|
|
250
|
-
reach the mesh once the `.visa` relay is wired (`SUBWAY_MESH` / `SUBWAY_RELAY_MULTIADDR`);
|
|
251
|
-
until then they no-op against an unreachable relay.
|
|
252
|
-
- **Real-merchant card checkout (experimental, opt-in)** — `pay_merchant` fills and pays an
|
|
253
|
-
ordinary merchant web checkout with a **Verified Agent card credential**: a one-shot
|
|
254
|
-
network-token cryptogram minted **on this device** (non-custodial) — not a stored card,
|
|
255
|
-
not x402, not server-side spend controls. Two steps: `review` (free; returns merchant +
|
|
256
|
-
exact amount as a `reviewId`) then `pay` (requires `confirm: "PAY <reviewId>"` + a passkey,
|
|
257
|
-
and CHARGES). **Tester-gated** (`checkout_agent_access`) and **RC/preview builds only** —
|
|
258
|
-
opt-in, never paired-and-go.
|
|
259
|
-
|
|
260
|
-
If a tool you expect isn't visible, the MCP server isn't mounted (or the v4 wallet runtime
|
|
261
|
-
isn't bundled in this build) — go back to "Getting set up".
|
|
262
|
-
|
|
263
|
-
## Important
|
|
264
|
-
|
|
265
|
-
- **The confirmation code is display-only.** Show it; never ask for it; never accept it as
|
|
266
|
-
input. It lets the human verify the page matches the flow you started.
|
|
267
|
-
- **Never read, print, log, or echo** the credential file or the enrollment pending file,
|
|
268
|
-
or the `browserUrl` query values beyond the single presentation to the user. The link
|
|
269
|
-
carries only a hash + a public key — safe in history — but treat it as one-time.
|
|
270
|
-
- Do not run API-key setup, card enrollment, or a balance top-up as a substitute for
|
|
271
|
-
pairing. Once paired, use the `visa-cli` skill's Rail 1 commands to pay.
|
|
272
|
-
|
|
273
|
-
## Limits
|
|
274
|
-
|
|
275
|
-
| Limit | Value |
|
|
276
|
-
| ------------------------------ | ------------------------------------------------------------ |
|
|
277
|
-
| Hand-off validity | 15 minutes from `pair_agent_start` |
|
|
278
|
-
| In-flight hand-offs per device | 1 (a new start replaces the prior) |
|
|
279
|
-
| Claim | single-shot server-side; once claimed the entry is destroyed |
|
|
280
|
-
| Confirmation code | 6 chars, no ambiguous glyphs; display-only |
|
|
281
|
-
|
|
282
|
-
## Errors
|
|
283
|
-
|
|
284
|
-
All errors are JSON with a non-zero exit code; `enroll-claim` tags them with `status`.
|
|
285
|
-
|
|
286
|
-
| status / symptom | Cause | Recovery |
|
|
287
|
-
| ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
288
|
-
| `error: verify-web URL not available in stable builds` | Stable CLI build, surface not public | Set `VISA_VERIFY_WEB_URL` or use a preview build. Do not proceed otherwise. |
|
|
289
|
-
| `error: RC build requires access` (may say `Run: visa-cli setup`) | Older RC build whose employee gate still fires on the bootstrap | **Do NOT run `visa-cli setup`** (legacy v3 GitHub-OAuth path; dead-ends on the same gate). Upgrade the RC (`npm install -g @visa/cli@rc`) or set `VISA_RC_CODE`, then retry the hand-off. |
|
|
290
|
-
| `no_pending` | No hand-off in flight (never started, or already claimed/expired) | Start fresh with `pair_agent_start`. |
|
|
291
|
-
| `not_ready`, `likelyExpired: false` | User hasn't finished the mobile flow | Wait, tell the user, poll again. Bounded polling only. |
|
|
292
|
-
| `not_ready`, `likelyExpired: true` | 15-minute window elapsed | Start over with `pair_agent_start`. |
|
|
293
|
-
| `confirm_mismatch` | The claim didn't match THIS device's secret | STOP. Show the re-derived `confirmCode`. If the user did not just finish the flow, someone else may hold their link — start over. Never retry blindly. |
|
|
294
|
-
| `error` (network / malformed) | Transport or server error | Surface the message. Poll once more; if it persists, start over. |
|
|
295
|
-
|
|
296
|
-
## Further docs
|
|
297
|
-
|
|
298
|
-
- `docs/agents/ARCHITECTURE.md` — where the enroll hand-off sits in the v4 request paths.
|
|
299
|
-
- `visacli.sh/agents` — product-facing agent docs.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Portable provisioner for the pair-visa-agent skill.
|
|
3
|
-
//
|
|
4
|
-
// Ensures the `visa` CLI is installed so ANY Agent Skills runtime can pair —
|
|
5
|
-
// not just OpenClaw (whose `metadata.openclaw.install` auto-runs). Hermes,
|
|
6
|
-
// Claude Code, and any other agentskills.io-compatible runtime run this bundled
|
|
7
|
-
// script per the standard's `scripts/` execution stage.
|
|
8
|
-
//
|
|
9
|
-
// Safe to run repeatedly: it no-ops when `visa` already resolves. Pinned to @rc
|
|
10
|
-
// because the v4 agent surface is prerelease (the @latest tag predates the
|
|
11
|
-
// `visa agent` commands); drop the tag once 4.1.0 is promoted to latest.
|
|
12
|
-
|
|
13
|
-
import { execSync } from 'node:child_process'
|
|
14
|
-
|
|
15
|
-
function resolves(cmd) {
|
|
16
|
-
try {
|
|
17
|
-
execSync(`${cmd} --version`, { stdio: 'ignore' })
|
|
18
|
-
return true
|
|
19
|
-
} catch {
|
|
20
|
-
return false
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (resolves('visa') || resolves('visa-cli')) {
|
|
25
|
-
console.log('✓ visa CLI already installed — nothing to do. Run the pairing flow in SKILL.md.')
|
|
26
|
-
process.exit(0)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
console.log('Installing @visa/cli@rc (the v4 agent surface is prerelease)…')
|
|
30
|
-
try {
|
|
31
|
-
execSync('npm install -g @visa/cli@rc', { stdio: 'inherit' })
|
|
32
|
-
} catch {
|
|
33
|
-
console.error(
|
|
34
|
-
'Global install failed. Try `npm install -g @visa/cli@rc` manually (may need sudo, or set a\n' +
|
|
35
|
-
'user-writable npm prefix: `npm config set prefix ~/.npm-global` and add its `bin` to PATH).'
|
|
36
|
-
)
|
|
37
|
-
process.exit(1)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!resolves('visa') && !resolves('visa-cli')) {
|
|
41
|
-
console.error(
|
|
42
|
-
'Installed, but `visa` is not on PATH. Ensure your npm global bin dir is on PATH\n' +
|
|
43
|
-
'(`npm bin -g` shows it), then re-run this script.'
|
|
44
|
-
)
|
|
45
|
-
process.exit(1)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log('✓ visa CLI ready. Now run the pairing flow in SKILL.md.')
|