diff --git a/package.json b/package.json index 1acb78c2..892c3644 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "author": "", "license": "MIT", "dependencies": { - "@contextvm/sdk": "^0.11.14", + "@contextvm/sdk": "^0.13.0", "@modelcontextprotocol/sdk": "^1.27.1", "json-schema-to-typescript": "15.0.4", "nostr-tools": "^2.23.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c0a4520..e726e649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,8 +8,8 @@ importers: .: dependencies: '@contextvm/sdk': - specifier: ^0.11.14 - version: 0.11.14(typescript@5.9.3) + specifier: ^0.13.0 + version: 0.13.0(typescript@5.9.3) '@modelcontextprotocol/sdk': specifier: ^1.27.1 version: 1.27.1(zod@4.3.6) @@ -241,10 +241,19 @@ packages: integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==, } - '@contextvm/sdk@0.11.14': + '@contextvm/mcp-sdk@1.30.0': resolution: { - integrity: sha512-QTRReNJ3NP6kOT2umHYmhYLSUBK4PtX5wFy4MUm7U/CJq1vnmPnmQ3HnHTG+gNtyLNiv4I3epuUPA6MJm3K/WA==, + integrity: sha512-Z4xyxxxtKnczEgVzLagaUdT1EtoVzc1bjr6kpIfTlw7mP5ESAzXeDUAMO9leIwituphz2ELE6ad9BjW7blDjjw==, + } + engines: { node: '>=18' } + peerDependencies: + zod: ^3.25 || ^4.0 + + '@contextvm/sdk@0.13.0': + resolution: + { + integrity: sha512-XPD/hG+41HbTSTgB1FgBgXfqDJRohj4HxiIMVea8irZ/qCqjVs4G0pqHrbI/XmAgEA2lHrsEGOTwA1yyjsXfPg==, } engines: { bun: '>=1.2.0' } peerDependencies: @@ -591,19 +600,6 @@ packages: '@cfworker/json-schema': optional: true - '@modelcontextprotocol/sdk@1.29.0': - resolution: - { - integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==, - } - engines: { node: '>=18' } - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@napi-rs/wasm-runtime@1.1.1': resolution: { @@ -1414,16 +1410,16 @@ packages: } engines: { node: '>=12' } - applesauce-core@5.2.0: + applesauce-core@6.1.0: resolution: { - integrity: sha512-aSuM6q6/Gs2FGUqytlHDjKZpSst2xKaT0vMXUQFWUctECNIxvwy6/hTDDInukMuI9mrQdjnO781ZJJgghI7RNw==, + integrity: sha512-+tOFNP/54Zz3Z2tKeCqQZRdFQp0bwwPRiYO4A/PKOmx9iHO2XOFMrPEuRGtNqaHeBmDQQn0pFNB9mif0t94g8w==, } - applesauce-relay@5.2.0: + applesauce-relay@6.0.3: resolution: { - integrity: sha512-ty8PzHenocGdTr3x3It8Ql0rMD9rxB6VGCzGRfL5QF6epdstv2YHKuTyr8QdPBvf7yxfc7oZcMi6djSwNxXqkQ==, + integrity: sha512-AqJMKaCwxljwRCr3cR3iaB8r+adl1+DNzeLpoXX9+locBENQaIdDbV5Kk2D0o1gcI1rGpvx9bGVUyUs32Z4rog==, } argparse@1.0.10: @@ -3714,11 +3710,19 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@contextvm/sdk@0.11.14(typescript@5.9.3)': + '@contextvm/mcp-sdk@1.30.0(zod@4.4.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + cross-spawn: 7.0.6 + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) + + '@contextvm/sdk@0.13.0(typescript@5.9.3)': dependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + '@contextvm/mcp-sdk': 1.30.0(zod@4.4.3) '@noble/hashes': 2.2.0 - applesauce-relay: 5.2.0(typescript@5.9.3) + applesauce-relay: 6.0.3(typescript@5.9.3) canonicalize: 2.1.0 nostr-tools: 2.18.2(typescript@5.9.3) pino: 10.3.1 @@ -3727,7 +3731,6 @@ snapshots: ws: 8.20.0 zod: 4.4.3 transitivePeerDependencies: - - '@cfworker/json-schema' - bufferutil - supports-color - utf-8-validate @@ -3899,28 +3902,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': - dependencies: - '@hono/node-server': 1.19.11(hono@4.12.8) - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 8.3.1(express@5.2.1) - hono: 4.12.8 - jose: 6.2.2 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.4.3 - zod-to-json-schema: 3.25.1(zod@4.4.3) - transitivePeerDependencies: - - supports-color - '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.9.1 @@ -4274,7 +4255,7 @@ snapshots: ansi-styles@6.2.3: {} - applesauce-core@5.2.0(typescript@5.9.3): + applesauce-core@6.1.0(typescript@5.9.3): dependencies: debug: 4.4.3 fast-deep-equal: 3.1.3 @@ -4286,10 +4267,10 @@ snapshots: - supports-color - typescript - applesauce-relay@5.2.0(typescript@5.9.3): + applesauce-relay@6.0.3(typescript@5.9.3): dependencies: '@noble/hashes': 1.8.0 - applesauce-core: 5.2.0(typescript@5.9.3) + applesauce-core: 6.1.0(typescript@5.9.3) nanoid: 5.1.7 nostr-tools: 2.19.4(typescript@5.9.3) rxjs: 7.8.2 diff --git a/src/call.ts b/src/call.ts index 31fc92fa..ab50882e 100644 --- a/src/call.ts +++ b/src/call.ts @@ -15,6 +15,9 @@ import { generatePrivateKey, normalizePrivateKey, normalizePublicKey } from './u import { BOLD, CYAN, DIM, RESET, TEXT } from './constants/ui.ts'; import { renderDefaultResult } from './call/render-result.ts'; import { renderSchemaProperties, renderToolSchema } from './call/render-schema.ts'; +import { withClientPayments, PMI_BITCOIN_LIGHTNING_BOLT11 } from '@contextvm/sdk/payments'; +import type { PaymentInteractionMode } from '@contextvm/sdk/payments'; +import { CliPaymentHandler } from './payments/cli-payment-handler.ts'; const HEX_PUBKEY_PATTERN = /^[0-9a-f]{64}$/i; @@ -37,6 +40,7 @@ export interface CallOptions { prettyRaw?: boolean; extract?: string; help?: boolean; + paymentMode?: PaymentInteractionMode; } export interface ParseCallResult { @@ -56,6 +60,7 @@ export interface ParseCallResult { showServerDetails: boolean; config: string | undefined; unknownFlags: string[]; + paymentMode: PaymentInteractionMode; } interface ResolvedServerTarget { @@ -118,6 +123,7 @@ export function parseCallArgs(args: string[]): ParseCallResult { showServerDetails: false, config: undefined, unknownFlags: [], + paymentMode: 'transparent', }; for (let i = 0; i < args.length; i++) { @@ -167,6 +173,13 @@ export function parseCallArgs(args: string[]): ParseCallResult { result.isStateless = false; } else if (arg === '--details') { result.showServerDetails = true; + } else if (arg === '--payment-mode') { + const value = consumeValue('--payment-mode'); + if (value === 'transparent' || value === 'explicit_gating') { + result.paymentMode = value; + } else { + result.unknownFlags.push(`--payment-mode${value ? ` (${value})` : ''}`); + } } else if (arg.startsWith('--')) { result.unknownFlags.push(arg); } else if (!result.server) { @@ -511,8 +524,18 @@ async function createRemoteClient(target: ResolvedServerTarget, options: CallOpt logLevel: options.debug ? 'debug' : 'silent', }); + const cliHandler = new CliPaymentHandler({ + pmi: PMI_BITCOIN_LIGHTNING_BOLT11, + verbose: options.verbose, + }); + + const paidTransport = withClientPayments(transport, { + handlers: [cliHandler], + paymentInteraction: options.paymentMode ?? 'transparent', + }); + const client = new Client({ name: 'cvmi', version: '0.1.0' }); - await client.connect(transport); + await client.connect(paidTransport); return { client, @@ -718,6 +741,11 @@ function isMissingToolInvocationError(error: unknown): boolean { return /tool.+not found|unknown tool|method not found|-32601/i.test(error.message); } +function isPaymentRequiredError(error: unknown): boolean { + // CEP-8 Payment Required JSON-RPC error code + return error instanceof Error && 'code' in error && (error as any).code === -32042; +} + export async function call( serverArg: string | undefined, capabilityArg: string | undefined, @@ -793,6 +821,10 @@ export async function call( ); } catch (error) { if (!isMissingToolInvocationError(error)) { + if (options.paymentMode === 'explicit_gating' && isPaymentRequiredError(error)) { + console.log(JSON.stringify((error as any).data, null, 2)); + process.exit(2); + } throw error; } @@ -845,6 +877,7 @@ ${BOLD}Options:${RESET} --private-key Your Nostr private key (hex/nsec format, overrides env, auto-generated if not provided) --relays Comma-separated relay URLs --encryption-mode Encryption mode: optional, required, disabled + --payment-mode Payment interaction mode: transparent (default), explicit_gating --stateless Enable stateless transport mode (default) --stateful Disable stateless transport mode --details Show resolved server identity and relay details during inspection diff --git a/src/payments/cli-payment-handler.ts b/src/payments/cli-payment-handler.ts new file mode 100644 index 00000000..10e27d85 --- /dev/null +++ b/src/payments/cli-payment-handler.ts @@ -0,0 +1,43 @@ +import type { PaymentHandler, PaymentHandlerRequest } from '@contextvm/sdk/payments'; +import { BOLD, CYAN, DIM, RESET, TEXT } from '../constants/ui.ts'; + +/** + * CLI payment handler for transparent mode. + * + * - Lets the SDK advertise the PMI via `pmi` tags (CEP-8 discovery) + * - Captures `payment_required` and renders the invoice in the terminal + * - Does NOT attempt to pay (human pays out-of-band) + */ +export class CliPaymentHandler implements PaymentHandler { + public readonly pmi: string; + private readonly verbose: boolean; + + constructor(options: { pmi: string; verbose?: boolean }) { + this.pmi = options.pmi; + this.verbose = options.verbose ?? false; + } + + canHandle(_req: PaymentHandlerRequest): boolean { + return true; + } + + async handle(req: PaymentHandlerRequest): Promise { + console.error(); + console.error(`${BOLD}⚡ Payment Required${RESET}`); + console.error(`${DIM}${'─'.repeat(50)}${RESET}`); + console.error(` ${CYAN}Amount:${RESET} ${req.amount}`); + if (req.description) { + console.error(` ${CYAN}Description:${RESET} ${req.description}`); + } + console.error(` ${CYAN}PMI:${RESET} ${req.pmi}`); + if (req.ttl) { + console.error(` ${CYAN}Expires in:${RESET} ${req.ttl}s`); + } + console.error(`${DIM}${'─'.repeat(50)}${RESET}`); + console.error(` ${CYAN}Invoice:${RESET}`); + console.error(` ${TEXT}${req.pay_req}${RESET}`); + console.error(`${DIM}${'─'.repeat(50)}${RESET}`); + console.error(); + console.error(`${DIM}Pay the invoice above. The CLI is waiting for confirmation...${RESET}`); + } +}