diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index ad80a36..efc8cc5 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -5,6 +5,7 @@ import { createDemoCli } from './commands/demo'; import { createMppCli } from './commands/mpp'; import { createOnboardCli } from './commands/onboard'; import { createPaymentMethodsCli } from './commands/payment-methods'; +import { createReportCli } from './commands/report'; import { createShippingAddressCli } from './commands/shipping-address'; import { createSpendRequestCli } from './commands/spend-request'; import { createUserInfoCli } from './commands/user-info'; @@ -86,6 +87,7 @@ cli.command(createMppCli(spendRequestRepo, authStorage)); cli.command( createWebBotAuthCli(() => factory.createWebBotAuthResource(), authStorage), ); +cli.command(createReportCli(() => factory.createReportResource(), authStorage)); cli.command( createDemoCli( authRepo, diff --git a/packages/cli/src/commands/report/index.tsx b/packages/cli/src/commands/report/index.tsx new file mode 100644 index 0000000..ba602e3 --- /dev/null +++ b/packages/cli/src/commands/report/index.tsx @@ -0,0 +1,69 @@ +import type { AuthStorage, IReportResource } from '@stripe/link-sdk'; +import { Cli, z } from 'incur'; +import { requireAuth } from '../../utils/require-auth'; + +const VALID_OUTCOMES = ['success', 'blocked', 'abandoned'] as const; +const VALID_TAGS = [ + 'stripe_checkout', + 'captcha', + 'anti_bot_script', + 'cdn_block', + 'waf_block', + 'dns_block', + 'rate_limited', + 'login_required', + '3ds_challenge', + 'page_inaccessible', + 'timeout', + 'site_error', + 'payment_declined', + 'other', +] as const; + +const reportOptions = z.object({ + domain: z.string().describe('Domain where the outcome occurred'), + outcome: z + .enum(VALID_OUTCOMES) + .describe('What happened: success, blocked, or abandoned'), + spendRequestId: z.string().describe('Spend request ID (lsrq_...)'), + tag: z + .array(z.enum(VALID_TAGS)) + .optional() + .describe('Outcome tags (repeatable)'), + step: z.string().optional().describe('Where in the flow the agent was'), + freeformContext: z + .string() + .optional() + .describe('Additional context (max 500 chars)'), +}); + +export function createReportCli( + createResource: () => IReportResource, + authStorage?: AuthStorage, +) { + const cli = Cli.create('report', { + description: 'Report the outcome of a purchase attempt', + }); + + cli.command('', { + description: + 'Report the outcome of an agent action on a domain. Call after every purchase attempt.', + options: reportOptions, + outputPolicy: 'agent-only' as const, + middleware: [requireAuth(authStorage)], + async run(c) { + const resource = createResource(); + const result = await resource.create({ + domain: c.options.domain, + outcome: c.options.outcome, + spend_request_id: c.options.spendRequestId, + tags: c.options.tag, + step: c.options.step, + freeform_context: c.options.freeformContext, + }); + return result; + }, + }); + + return cli; +} diff --git a/packages/cli/src/utils/resource-factory.ts b/packages/cli/src/utils/resource-factory.ts index 8ba5b17..b14eec0 100644 --- a/packages/cli/src/utils/resource-factory.ts +++ b/packages/cli/src/utils/resource-factory.ts @@ -1,11 +1,13 @@ import { type AuthStorage, type IPaymentMethodsResource, + type IReportResource, type IShippingAddressResource, type ISpendRequestResource, type IUserInfoResource, type IWebBotAuthResource, PaymentMethodsResource, + ReportResource, ShippingAddressResource, SpendRequestResource, UserInfoResource, @@ -70,6 +72,7 @@ export class ResourceFactory { private shippingAddressResource?: IShippingAddressResource; private userInfoResource?: IUserInfoResource; private webBotAuthResource?: IWebBotAuthResource; + private reportResource?: IReportResource; constructor(options: ResourceFactoryOptions = {}) { this.verbose = options.verbose ?? false; @@ -192,4 +195,21 @@ export class ResourceFactory { return this.webBotAuthResource; } + + createReportResource(): IReportResource { + if (this.reportResource) { + return this.reportResource; + } + + const getAccessToken = this.createSdkAccessTokenProvider(); + this.reportResource = sanitizeResource( + new ReportResource({ + verbose: this.verbose, + defaultHeaders: this.defaultHeaders, + getAccessToken, + }), + ); + + return this.reportResource; + } }