Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
69 changes: 69 additions & 0 deletions packages/cli/src/commands/report/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 20 additions & 0 deletions packages/cli/src/utils/resource-factory.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
type AuthStorage,
type IPaymentMethodsResource,
type IReportResource,
type IShippingAddressResource,
type ISpendRequestResource,
type IUserInfoResource,
type IWebBotAuthResource,
PaymentMethodsResource,
ReportResource,
ShippingAddressResource,
SpendRequestResource,
UserInfoResource,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Loading