Skill: jpbonch/selva (version 1.0.0)

--- name: Selva description: Shopping platform for AI agents --- # Selva Shopping platform for AI agents. Search, compare, and buy physical products from Amazon. ## User Experience - Be concise and human. Do not narrate command execution unless asked. - Prefer outcome-first messaging: - Good: "You’re registered. I can search now. Want me to find options for `<item>`?" - Avoid: "CLI installed... API key saved... next command is..." - Ask for only the missing info needed for the next step. - When sharing links, use friendly markdown labels when supported: - `[Open settings page](https://...)` - When listing products for chat clients, send each product as a separate message with: - the product image as a photo attachment - a short caption (title, price, rating/reviews, delivery) - a final text message asking which item to open or buy ## Default Conversation Flow 1. **Register quietly** - If registration is needed, do it and confirm briefly. 2. **Check readiness** - If name or address is missing, ask for them naturally. - If card is missing and user wants to buy, offer the settings link. 3. **Search and guide** - Present top choices clearly (title, price, rating, delivery/get-it-by, image url, quick reason). - Ask a simple follow-up: "Want details on any of these?" 4. **Buy** - Confirm selected item and payment method. - If approval flow is enabled and triggered, explain next action briefly. ## Setup 1. `npx selva-cli register` — generates an API key, stored locally at ~/selva/config.json 2. `npx selva-cli settings set-name --name "Jane Doe"` — required before buying 3. `npx selva-cli settings set-address --street "123 Main St" --line2 "Apt 4B" --city "Austin" --state "TX" --zip "78701" --country "US"` — required before buying (`--line2` optional) 4. Optionally set phone: `npx selva-cli settings set-phone --phone "+14155551234"` 5. Optionally set email for purchase receipts and approval notifications: `npx selva-cli settings set-email --email "[REDACTED_SECRET]"` 6. Link a payment card and optionally set an approval threshold at the web settings page: `npx selva-cli settings page` # Commands ### Search `npx selva-cli search "<query>"` Returns up to 10 normalized results with `selva_id`, title, price, rating, source, url, `delivery_estimate` (get-it-by), and `image_url`. When presenting search results to users, always include delivery/get-it-by text. Prefer separate photo messages with captions (one per item) over markdown image links. Requires address for best results but works without one. ### Details `npx selva-cli details <selva_id>` Returns full product details from the original provider. Use the `selva_id` from search results (e.g. `amzn_B0EXAMPLE`). ### Buy `npx selva-cli buy <selva_id> --method <saved|card>` Requires name, address, and a chargeable card profile. Buy creates an order record and returns its current status. If an approval threshold is configured and the price exceeds it, the order enters `awaiting_approval` and an approval email is sent. Options: - `--method saved` — uses the card linked on the settings page. Fails if no card is linked - ask the user to link one. - `--method card --number <num> --exp <MM/YY> --cvv <code>` — tokenizes with Stripe and saves a reusable payment method snapshot for later processing. Card details never reach the Selva API. ### Orders `npx selva-cli orders` Lists all orders with status: awaiting_approval, processing, expired, shipping. Status meaning: - `awaiting_approval`: waiting for user approval email click. - `processing`: order accepted and being processed. - `shipping`: shipping status has been updated. ### Settings - `npx selva-cli settings` — view current name, phone, address, email, approval threshold, and linked card info - `npx selva-cli settings page` — generates a 24-hour link to the web settings page where a human can link/remove a payment card and set an approval threshold - `npx selva-cli settings set-address --street <street> [--line2 <line2>] --city <city> --state <state> --zip <zip> --country <country>` - `npx selva-cli settings set-name --name <name>` - `npx selva-cli settings set-email --email <email>` - `npx selva-cli settings set-phone --phone <phone>` ## Product ID Format IDs are prefixed by provider: `amzn_` for Amazon. Pass these to `details` and `buy`. ## Capability Reference - Register user and store API key locally. - Search products. - Fetch product details. - Buy by `selva_id`. - View orders. - Manage settings (name, phone, address, email, settings page link). ## Command Reference - `npx selva-cli register` - `npx selva-cli search "<query>"` - `npx selva-cli details <selva_id>` - `npx selva-cli buy <selva_id> --method <saved|card>` - `npx selva-cli orders` - `npx selva-cli settings` - `npx selva-cli settings page` - `npx selva-cli settings set-address --street <street> [--line2 <line2>] --city <city> --state <state> --zip <zip> --country <country>` - `npx selva-cli settings set-name --name <name>` - `npx selva-cli settings set-email --email <email>` - `npx selva-cli settings set-phone --phone <phone>`

path: README.md
content: # Selva CLI

Shopping platform CLI for AI agents. Search, inspect, and buy physical products from Amazon through a single interface.

## Quick start

1. Register an API key:

```bash
npx selva-cli register
```

2. Set name (required before buying):

```bash
npx selva-cli settings set-name --name "Jane Doe"
```

3. Set address (required before buying, `--line2` optional):

```bash
npx selva-cli settings set-address --street "123 Main St" --line2 "Apt 4B" --city "Austin" --state "TX" --zip "78701" --country "US"
```

4. Optionally set phone:

```bash
npx selva-cli settings set-phone --phone "+14155551234"
```

5. Optionally set email (for receipts and approval notifications):

```bash
npx selva-cli settings set-email --email "[REDACTED_SECRET]"
```

6. Link a card / optionally configure approval threshold via settings page:

```bash
npx selva-cli settings page
```

## Commands

### Search

```bash
npx selva-cli search "<query>"
```

Returns up to 10 normalized results with `selva_id`, title, price, rating, source, and url.

### Details

```bash
npx selva-cli details <selva_id>
```

Returns expanded product details for a result (for example `amzn_B0EXAMPLE`).

### Buy

```bash
npx selva-cli buy <selva_id> --method <saved|card>
```

Requires name and address to be set before placing an order.
Buy creates an order record and returns current order status.

Options:

- `--method saved` uses the linked card from settings page.
- `--method card --number <num> --exp <MM/YY> --cvv <code>` tokenizes with Stripe and saves a reusable payment method snapshot for later processing.

### Orders

```bash
npx selva-cli orders
```

Lists all orders with status (`awaiting_approval`, `processing`, `expired`, `shipping`).
- `awaiting_approval`: waiting for approval email action.
- `processing`: order accepted and being processed.
- `shipping`: shipping status updated.

### Settings

- `npx selva-cli settings`
- `npx selva-cli settings page`
- `npx selva-cli settings set-address --street <street> [--line2 <line2>] --city <city> --state <state> --zip <zip> --country <country>`
- `npx selva-cli settings set-name --name <name>`
- `npx selva-cli settings set-email --email <email>`
- `npx selva-cli settings set-phone --phone <phone>`


path: package.json
content: {
  "name": "selva-cli",
  "version": "0.1.1",
  "private": false,
  "type": "module",
  "bin": {
    "selva": "dist/index.js"
  },
  "files": [
    "dist",
    "SKILL.md"
  ],
  "scripts": {
    "dev": "tsx src/dev-index.ts",
    "build": "tsc -p tsconfig.json",
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "dependencies": {
    "commander": "^14.0.0",
    "zod": "^4.1.5"
  },
  "devDependencies": {
    "@types/node": "^24.3.0",
    "tsx": "^4.20.5",
    "typescript": "^5.9.2",
    "vitest": "^3.2.4"
  }
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "dist",
    "rootDir": "src",
    "types": ["node", "vitest/globals"]
  },
  "include": ["src"]
}


path: src/stripe.ts
content: export function parseExpiry(exp: string) {
  const match = exp.match(/^(\d{1,2})\/(\d{2}|\d{4})$/);
  if (!match) {
    throw new Error("Invalid expiry format. Use MM/YY or MM/YYYY.");
  }

  const monthPart = match[1];
  const yearPart = match[2];
  if (!monthPart || !yearPart) {
    throw new Error("Invalid expiry format. Use MM/YY or MM/YYYY.");
  }

  const month = Number.parseInt(monthPart, 10);
  if (month < 1 || month > 12) {
    throw new Error("Invalid expiry month.");
  }

  let year = Number.parseInt(yearPart, 10);
  if (year < 100) {
    year += 2000;
  }

  return { month, year };
}

export async function tokenizeCard(input: {
  publishableKey: string;
  number: string;
  exp: string;
  cvv: string;
}) {
  if (!input.publishableKey) {
    throw new Error("Stripe publishable key is missing from API response.");
  }

  const expiry = parseExpiry(input.exp);
  const body = new URLSearchParams({
    "card[number]": input.number,
    "card[exp_month]": String(expiry.month),
    "card[exp_year]": String(expiry.year),
    "card[cvc]": input.cvv
  });

  const auth = Buffer.from(`${input.publishableKey}:`).toString("base64");

  const response = await fetch("https://api.stripe.com/v1/tokens", {
    method: "POST",
    headers: {
      authorization: `Basic ${auth}`,
      "content-type": "application/x-www-form-urlencoded"
    },
    body
  });

  const json = (await response.json()) as Record<string, unknown>;

  if (!response.ok || !json.id) {
    const message =
      typeof json.error === "object" && json.error !== null && "message" in json.error
        ? String((json.error as { message?: unknown }).message)
        : `Stripe tokenization failed (${response.status})`;
    throw new Error(message);
  }

  return { [REDACTED_SECRET]), raw: json };
}


path: src/dev-index.ts
content: #!/usr/bin/env node
import { setApiBaseUrl } from "./config.js";
import { runCli } from "./cli.js";

setApiBaseUrl("http://localhost:3000");

runCli(process.argv).catch((error) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});


path: src/cli.ts
content: import { Command } from "commander";
import {
  buy,
  details,
  orders,
  register,
  search,
  setAddress,
  setEmail,
  setName,
  setPhone,
  settingsPageLink,
  settingsSummary,
  stripePublishableKey
} from "./api.js";
import { pretty, money, printSection } from "./format.js";
import { readConfig, writeConfig } from "./config.js";
import { tokenizeCard } from "./stripe.js";

function amount(value: { amount: number } | null | undefined) {
  return value?.amount ?? null;
}

export async function runCli(argv: string[]) {
  const program = new Command();
  program.name("selva").description("Selva shopping CLI").version("0.1.0");

  program
    .command("register")
    .description("Register and store a new API key")
    .action(async () => {
      const response = await register();
      const existing = await readConfig();

      await writeConfig({
        ...existing,
        [REDACTED_SECRET]
      });

      console.log(`Registered. API key saved to ~/selva/config.json`);
      console.log(response.message);
    });

  program
    .command("search")
    .description("Search products")
    .argument("<query>", "Search query")
    .option("--raw", "Include full raw provider payloads")
    .action(async (query: string, options: { raw?: boolean }) => {
      const response = await search(query, { includeRaw: options.raw === true });

      if (response.notice) {
        console.log(response.notice);
      }

      printSection("Search Results");
      if (response.pricing_note) {
        console.log(`(${response.pricing_note})`);
      }
      if (!response.products.length) {
        console.log("No products found.");
      }

      for (const [index, item] of response.products.entries()) {
        console.log(`${index + 1}. ${item.selva_id}`);
        console.log(`   ${item.title}`);
        console.log(`   source: ${item.provider} | price: ${money(amount(item.price))}`);
        console.log(`   rating: ${item.rating ?? "n/a"} | reviews: ${item.review_count ?? "n/a"}`);
        const searchMeta = [`delivery: ${item.delivery_estimate ?? "n/a"}`];
        if (item.availability_status !== "unknown") {
          searchMeta.unshift(`availability: ${item.availability_status}`);
        }
        console.log(`   ${searchMeta.join(" | ")}`);
        console.log(`   image: ${item.image_url ?? "n/a"}`);
        console.log(`   url: ${item.url ?? "n/a"}`);
      }

      if (response.errors.length) {
        printSection("Provider Errors");
        for (const error of response.errors) {
          console.log(`${error.provider}: ${error.message}`);
        }
      }

      if (response.raw_providers) {
        printSection("Raw Provider Responses");
        console.log(pretty(response.raw_providers));
      }
    });

  program
    .command("details")
    .description("Get product details")
    .argument("<selva_id>", "Selva product id")
    .action(async (selvaId: string) => {
      const response = await details(selvaId);

      printSection("Product Details");
      if (response.pricing_note) {
        console.log(`(${response.pricing_note})`);
      }
      const p = response.product;
      console.log(`selva_id: ${p.selva_id}`);
      console.log(`title: ${p.title}`);
      console.log(`provider: ${p.provider}`);
      console.log(`price: ${money(amount(p.price))} ${p.price?.currency ?? "USD"}`);
      if (p.price_range?.min || p.price_range?.max) {
        console.log(
          `price_range: min=${money(amount(p.price_range?.min))} max=${money(amount(p.price_range?.max))}`
        );
      }
      console.log(`rating: ${p.rating ?? "n/a"} | reviews: ${p.review_count ?? "n/a"}`);
      const detailMeta = [`delivery: ${p.delivery_estimate ?? "n/a"}`, `prime: ${p.prime_eligible ?? "n/a"}`];
      if (p.availability_status !== "unknown") {
        detailMeta.unshift(`availability: ${p.availability_status} (${p.stock_status_text ?? "n/a"})`);
      }
      console.log(detailMeta.join(" | "));
      console.log(`url: ${p.url}`);
      console.log(`image: ${p.image_url ?? "n/a"}`);
      if (p.images?.length) {
        console.log(`images: ${p.images.join(", ")}`);
      }
      if (p.brand) {
        console.log(`brand: ${p.brand}`);
      }
      if (p.merchant) {
        console.log(`merchant: ${p.merchant.name ?? "n/a"} (${p.merchant.url ?? "n/a"})`);
      }
      if (p.description) {
        console.log(`description: ${p.description}`);
      }
      if (p.item_specifications?.length) {
        printSection("Item Specifications");
        for (const line of p.item_specifications.slice(0, 12)) {
          console.log(`- ${line}`);
        }
      }
      if (p.user_reviews?.length) {
        printSection("User Reviews");
        for (const review of p.user_reviews.slice(0, 5)) {
          const heading = `${review.rating != null ? `${review.rating}/5` : "n/a"}${review.author ? ` by ${review.author}` : ""}${review.title ? ` - ${review.title}` : ""}`;
          console.log(`- ${heading}`);
          console.log(`  ${review.text}`);
        }
      }
      if (p.features?.length) {
        console.log(`features: ${p.features.join(" | ")}`);
      }
      if (p.badges?.length) {
        console.log(`badges: ${p.badges.join(", ")}`);
      }
      if (p.attributes && Object.keys(p.attributes).length) {
        printSection("Detail Attributes");
        for (const [key, values] of Object.entries(p.attributes)) {
          console.log(`${key}: ${values.join(", ")}`);
        }
      }
      printSection("Detail Variants");
      if (!p.variants?.length) {
        console.log("none");
      } else {
        for (const variant of p.variants.slice(0, 12)) {
          console.log(
            `- ${variant.variant_id} | ${variant.title ?? "n/a"} | ${money(amount(variant.price))} ${variant.price?.currency ?? "USD"} | ${variant.availability_status}`
          );
          if (variant.options && Object.keys(variant.options).length) {
            console.log(
              `  options: ${Object.entries(variant.options)
                .map(([k, v]) => `${k}=${v}`)
                .join(", ")}`
            );
          }
          if (variant.checkout_url) {
            console.log(`  checkout: ${variant.checkout_url}`);
          }
        }
        if (p.variants.length > 12) {
          console.log(`...and ${p.variants.length - 12} more`);
        }
      }
    });

  program
    .command("buy")
    .description("Buy a product")
    .argument("<selva_id>", "Selva product id")
    .requiredOption("--method <method>", "Payment method: card|saved")
    .option("--number <card_number>", "Card number for --method card")
    .option("--exp <exp>", "Card expiry MM/YY for --method card")
    .option("--cvv <cvv>", "Card CVV for --method card")
    .action(
      async (
        selvaId: string,
        options: { method: string; number?: string; exp?: string; cvv?: string }
      ) => {
        const method = options.method === "saved" ? "saved" : options.method === "card" ? "card" : null;
        if (!method) {
          throw new Error("--method must be either 'card' or 'saved'.");
        }

        let paymentToken: string | undefined;
        if (method === "card") {
          if (!options.number || !options.exp || !options.cvv) {
            throw new Error("For --method card, provide --number, --exp, and --cvv.");
          }

          const stripeConfig = await stripePublishableKey();
          const publishableKey = stripeConfig.stripe_publishable_key;
          if (!publishableKey) {
            throw new Error("Card tokenization requires STRIPE_PUBLISHABLE_KEY on the API.");
          }

          const tokenized = await tokenizeCard({
            publishableKey,
            number: options.number,
            exp: options.exp,
            cvv: options.cvv
          });

          paymentToken = tokenized.token;
        }

        const response = await buy({
          selva_id: selvaId,
          method,
          payment_token: paymentToken
        });

        printSection("Buy Response");
        const body = response as Record<string, unknown>;
        const orderId = typeof body.order_id === "string" ? body.order_id : null;
        const status = typeof body.status === "string" ? body.status : null;
        const message = typeof body.message === "string" ? body.message : null;
        const emailWarning = typeof body.email_warning === "string" ? body.email_warning : null;

        if (orderId) {
          console.log(`order_id: ${orderId}`);
        }
        if (status) {
          console.log(`status: ${status}`);
        }
        if (message) {
          console.log(`message: ${message}`);
        }
        if (emailWarning) {
          console.log(`email_warning: ${emailWarning}`);
        }

        const product = body.product;
        if (product && typeof product === "object") {
          const p = product as Record<string, unknown>;
          const priceRecord =
            p.price && typeof p.price === "object"
              ? (p.price as { amount?: unknown })
              : null;
          const priceAmount =
            typeof priceRecord?.amount === "number" ? priceRecord.amount : null;
          const availability =
            typeof p.availability_status === "string" ? p.availability_status : null;

          printSection("Product");
          console.log(`${typeof p.selva_id === "string" ? p.selva_id : "n/a"}`);
          console.log(`${typeof p.title === "string" ? p.title : "n/a"}`);
          console.log(
            `source: ${typeof p.provider === "string" ? p.provider : "n/a"} | price: ${money(priceAmount)}`
          );
          console.log(
            `rating: ${typeof p.rating === "number" ? p.rating : "n/a"} | reviews: ${
              typeof p.review_count === "number" ? p.review_count : "n/a"
            }`
          );
          const productMeta = [
            `delivery: ${typeof p.delivery_estimate === "string" ? p.delivery_estimate : "n/a"}`
          ];
          if (availability && availability !== "unknown") {
            productMeta.unshift(`availability: ${availability}`);
          }
          console.log(productMeta.join(" | "));
          console.log(`image: ${typeof p.image_url === "string" ? p.image_url : "n/a"}`);
          console.log(`url: ${typeof p.url === "string" ? p.url : "n/a"}`);
        }

        if (!orderId && !status && (!product || typeof product !== "object")) {
          console.log(pretty(response));
        }
      }
    );

  program
    .command("orders")
    .description("List orders")
    .action(async () => {
      const response = await orders();
      printSection("Orders");
      if (!response.orders.length) {
        console.log("No orders yet.");
        return;
      }

      for (const order of response.orders) {
        console.log(`${order.id}`);
        console.log(`   selva_id: ${order.selva_id}`);
        console.log(`   status: ${order.status} | method: ${order.payment_method}`);
        console.log(
          `   subtotal: ${order.subtotal_amount ? `$${order.subtotal_amount}` : "n/a"} | tax: ${order.tax_amount ? `$${order.tax_amount}` : "$0.00"} | shipping: ${order.shipping_amount ? `$${order.shipping_amount}` : "$0.00"} | total: ${order.total_amount ? `$${order.total_amount}` : "n/a"} ${order.currency}`
        );
        if (order.failure_reason) {
          console.log(`   failure_reason: ${order.failure_reason}`);
        }
        console.log(`   created_at: ${order.created_at}`);
      }
    });

  const settings = program.command("settings").description("Manage and view settings");

  settings.action(async () => {
    const response = await settingsSummary();
    const card = response.settings.card;
    const address = response.settings.address;
    const addressParts = address
      ? [
          address.street,
          address.line2 ?? null,
          address.city,
          address.state,
          `${address.zip} ${address.country}`
        ].filter((value): value is string => Boolean(value && value.trim()))
      : [];
    console.log(`name: ${response.settings.name ?? "unset"}`);
    console.log(`phone: ${response.settings.phone ?? "unset"}`);
    console.log(`address: ${addressParts.length ? addressParts.join(", ") : "unset"}`);
    console.log(
      `threshold_limit: ${
        response.settings.approval_enabled
          ? `$${response.settings.approval_threshold_amount ?? "n/a"} ${response.settings.approval_threshold_currency ?? "USD"}`
          : "unset"
      }`
    );
    console.log(`zip_code: ${response.settings.zip_code ?? "unset"}`);
    console.log(`payment_method: ${card ? `${card.issuer ?? "card"} **** ${card.last4 ?? "????"}` : "not linked"}`);
    console.log(`email: ${response.settings.email ?? "unset"}`);
  });

  settings
    .command("page")
    .description("Generate a settings page URL")
    .action(async () => {
      const response = await settingsPageLink();
      console.log(response.url);
    });

  settings
    .command("set-address")
    .requiredOption("--street <street>", "Street address")
    .option("--line2 <line2>", "Address line 2 (optional)")
    .requiredOption("--city <city>", "City")
    .requiredOption("--state <state>", "State")
    .requiredOption("--zip <zip>", "ZIP code")
    .requiredOption("--country <country>", "Country code")
    .action(
      async (options: {
        street: string;
        line2?: string;
        city: string;
        state: string;
        zip: string;
        country: string;
      }) => {
        await setAddress(options);
        console.log("Address updated.");
      }
    );

  settings
    .command("set-email")
    .requiredOption("--email <email>", "Notification email")
    .action(async (options: { email: string }) => {
      await setEmail(options.email);
      console.log("Email updated.");
    });

  settings
    .command("set-name")
    .requiredOption("--name <name>", "Full name")
    .action(async (options: { name: string }) => {
      await setName(options.name);
      console.log("Name updated.");
    });

  settings
    .command("set-phone")
    .requiredOption("--phone <phone>", "Phone number")
    .action(async (options: { phone: string }) => {
      await setPhone(options.phone);
      console.log("Phone updated.");
    });

  await program.parseAsync(argv);
}


path: src/api.ts
content: import { requireApiKey, resolveBaseUrl } from "./config.js";

export type Provider = "amazon" | "shopify";
export type AvailabilityStatus = "in_stock" | "out_of_stock" | "limited" | "unknown";

export interface Money {
  amount: number;
  currency: string;
  display?: string;
  compare_at_amount?: number | null;
}

export interface SelvaVariant {
  variant_id: string;
  title?: string | null;
  options?: Record<string, string>;
  price?: Money | null;
  image_url?: string | null;
  availability_status: AvailabilityStatus;
  checkout_url?: string | null;
}

export interface SelvaUserReview {
  title?: string | null;
  rating?: number | null;
  text: string;
  author?: string | null;
}

export interface SelvaProduct {
  selva_id: string;
  provider: Provider;
  provider_product_id: string;
  title: string;
  description?: string | null;
  url: string;
  image_url?: string | null;
  images?: string[];
  price?: Money | null;
  price_range?: { min?: Money; max?: Money } | null;
  rating?: number | null;
  review_count?: number | null;
  availability_status: AvailabilityStatus;
  stock_status_text?: string | null;
  delivery_estimate?: string | null;
  prime_eligible?: boolean | null;
  brand?: string | null;
  merchant?: { id?: string; name?: string; url?: string } | null;
  variants?: SelvaVariant[];
  attributes?: Record<string, string[]>;
  badges?: string[];
  features?: string[];
  user_reviews?: SelvaUserReview[];
  item_specifications?: string[];
}

async function parseResponse(response: Response) {
  const text = await response.text();
  let parsed: unknown = null;

  try {
    parsed = text.length ? JSON.parse(text) : null;
  } catch {
    parsed = { message: text };
  }

  if (!response.ok) {
    const message =
      typeof parsed === "object" && parsed && "message" in parsed
        ? String((parsed as { message: unknown }).message)
        : `Request failed with ${response.status}`;
    throw new Error(message);
  }

  return parsed;
}

export async function register() {
  const baseUrl = await resolveBaseUrl();
  const response = await fetch(`${baseUrl}/register`, {
    method: "POST"
  });
  return parseResponse(response) as Promise<{ [REDACTED_SECRET]; message: string }>;
}

export async function settingsSummary() {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/settings`, {
    headers: {
      "x-api-key": apiKey
    }
  });

  return parseResponse(response) as Promise<{
    settings: {
      name: string | null;
      phone: string | null;
      email: string | null;
      zip_code: string | null;
      address: {
        street: string;
        line2?: string | null;
        city: string;
        state: string;
        zip: string;
        country: string;
      } | null;
      approval_enabled: boolean;
      approval_threshold_amount: number | null;
      approval_threshold_currency: string;
      card: {
        issuer: string | null;
        last4: string | null;
      } | null;
    };
  }>;
}

export async function settingsPageLink() {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/settings/link`, {
    method: "POST",
    headers: {
      "x-api-key": apiKey
    }
  });

  return parseResponse(response) as Promise<{ url: string; expires_in_hours: number }>;
}

export async function setAddress(input: {
  street: string;
  line2?: string | null;
  city: string;
  state: string;
  zip: string;
  country: string;
}) {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/settings/address`, {
    method: "PUT",
    headers: {
      "content-type": "application/json",
      "x-api-key": apiKey
    },
    body: JSON.stringify(input)
  });

  return parseResponse(response);
}

export async function setName(name: string) {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/settings/name`, {
    method: "PUT",
    headers: {
      "content-type": "application/json",
      "x-api-key": apiKey
    },
    body: JSON.stringify({ name })
  });

  return parseResponse(response);
}

export async function setPhone(phone: string) {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/settings/phone`, {
    method: "PUT",
    headers: {
      "content-type": "application/json",
      "x-api-key": apiKey
    },
    body: JSON.stringify({ phone })
  });

  return parseResponse(response);
}

export async function setEmail(email: string) {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/settings/email`, {
    method: "PUT",
    headers: {
      "content-type": "application/json",
      "x-api-key": apiKey
    },
    body: JSON.stringify({ email })
  });

  return parseResponse(response);
}

export async function search(query: string, options?: { includeRaw?: boolean }) {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();
  const includeRaw = options?.includeRaw === true;

  const response = await fetch(`${baseUrl}/search`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": apiKey
    },
    body: JSON.stringify({ query, include_raw: includeRaw })
  });

  return parseResponse(response) as Promise<{
    notice?: string;
    pricing_note?: string;
    products: Array<{
      selva_id: string;
      provider: Provider;
      title: string;
      url: string;
      image_url?: string | null;
      price?: Money | null;
      rating?: number | null;
      review_count?: number | null;
      availability_status: AvailabilityStatus;
      delivery_estimate?: string | null;
      prime_eligible?: boolean | null;
    }>;
    raw_providers?: {
      amazon?: unknown;
      shopify?: unknown;
    };
    errors: Array<{ provider: Provider; message: string }>;
  }>;
}

export async function details(selvaId: string) {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/details/${encodeURIComponent(selvaId)}`, {
    headers: {
      "x-api-key": apiKey
    }
  });

  return parseResponse(response) as Promise<{
    product: SelvaProduct;
    pricing_note?: string;
  }>;
}

export async function buy(input: {
  selva_id: string;
  method: "card" | "saved";
  payment_token?: string;
}) {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/buy`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": apiKey
    },
    body: JSON.stringify(input)
  });

  return parseResponse(response) as Promise<Record<string, unknown>>;
}

export async function orders() {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/orders`, {
    headers: {
      "x-api-key": apiKey
    }
  });

  return parseResponse(response) as Promise<{
    orders: Array<{
      id: string;
      selva_id: string;
      status: string;
      payment_method: string;
      subtotal_amount: string | null;
      tax_amount: string | null;
      shipping_amount: string | null;
      total_amount: string | null;
      currency: string;
      failure_reason: string | null;
      created_at: string;
    }>;
  }>;
}

export async function stripePublishableKey() {
  const baseUrl = await resolveBaseUrl();
  const apiKey = await requireApiKey();

  const response = await fetch(`${baseUrl}/settings/stripe-publishable-key`, {
    headers: {
      "x-api-key": apiKey
    }
  });

  return parseResponse(response) as Promise<{
    stripe_publishable_key: string;
  }>;
}


path: src/format.ts
content: export function printSection(title: string) {
  console.log(`\n${title}`);
  console.log("-".repeat(title.length));
}

export function pretty(value: unknown) {
  return JSON.stringify(value, null, 2);
}

export function money(value: number | null) {
  if (value == null) {
    return "n/a";
  }

  return `$${value.toFixed(2)}`;
}


path: src/index.ts
content: #!/usr/bin/env node
import { setApiBaseUrl } from "./config.js";
import { runCli } from "./cli.js";

setApiBaseUrl("https://api.useselva.com");

runCli(process.argv).catch((error) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});


path: src/config.ts
content: import { mkdir, readFile, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";

export interface SelvaCliConfig {
  apiKey?: string;
}

let apiBaseUrl = "https://api.useselva.com";

export function configPath() {
  return path.join(homedir(), "selva", "config.json");
}

export async function readConfig(): Promise<SelvaCliConfig> {
  const filePath = configPath();

  try {
    const raw = await readFile(filePath, "utf8");
    return JSON.parse(raw) as SelvaCliConfig;
  } catch {
    return {};
  }
}

export async function writeConfig(next: SelvaCliConfig) {
  const filePath = configPath();
  const dir = path.dirname(filePath);
  await mkdir(dir, { recursive: true });
  await writeFile(filePath, JSON.stringify(next, null, 2), "utf8");
}

export async function requireApiKey() {
  const config = await readConfig();
  if (!config.apiKey) {
    throw new Error("API key not found. Run: selva register");
  }
  return config.apiKey;
}

export function setApiBaseUrl(value: string) {
  apiBaseUrl = value.replace(/\/$/, "");
}

export async function resolveBaseUrl() {
  return apiBaseUrl;
}


path: test/stripe.test.ts
content: import { describe, expect, it } from "vitest";
import { parseExpiry } from "../src/stripe.js";

describe("parseExpiry", () => {
  it("parses two digit year", () => {
    expect(parseExpiry("12/25")).toEqual({ month: 12, year: 2025 });
  });

  it("parses four digit year", () => {
    expect(parseExpiry("01/2031")).toEqual({ month: 1, year: 2031 });
  });

  it("rejects invalid format", () => {
    expect(() => parseExpiry("2025-01")).toThrowError(/Invalid expiry format/);
  });
});


path: test/buy-command.test.ts
content: import { afterEach, describe, expect, it, vi } from "vitest";

vi.mock("../src/api.js", () => ({
  buy: vi.fn(),
  details: vi.fn(),
  orders: vi.fn(),
  register: vi.fn(),
  search: vi.fn(),
  setAddress: vi.fn(),
  setEmail: vi.fn(),
  setName: vi.fn(),
  setPhone: vi.fn(),
  settingsPageLink: vi.fn(),
  settingsSummary: vi.fn(),
  stripePublishableKey: vi.fn()
}));

vi.mock("../src/stripe.js", () => ({
  tokenizeCard: vi.fn()
}));

import * as api from "../src/api.js";
import * as stripe from "../src/stripe.js";
import { runCli } from "../src/cli.js";

describe("buy command", () => {
  afterEach(() => {
    vi.restoreAllMocks();
    vi.clearAllMocks();
  });

  it("always tokenizes cards with stripe_publishable_key", async () => {
    (api.stripePublishableKey as any).mockResolvedValue({
      stripe_publishable_key: "pk_test_selva"
    });
    (stripe.tokenizeCard as any).mockResolvedValue({ [REDACTED_SECRET]" });
    (api.buy as any).mockResolvedValue({ order_id: "ord_1", status: "shipping" });
    vi.spyOn(console, "log").mockImplementation(() => {});

    await runCli([
      "node",
      "selva",
      "buy",
      "amzn_B0TEST",
      "--method",
      "card",
      "--number",
      "4242424242424242",
      "--exp",
      "12/34",
      "--cvv",
      "123"
    ]);

    expect(stripe.tokenizeCard).toHaveBeenCalledWith(
      expect.objectContaining({
        publishableKey: "pk_test_selva"
      })
    );
    expect(api.buy).toHaveBeenCalledWith(
      expect.objectContaining({
        selva_id: "amzn_B0TEST",
        method: "card",
        payment_token: "tok_test"
      })
    );
  });

  it("fails fast when STRIPE_PUBLISHABLE_KEY is missing", async () => {
    (api.stripePublishableKey as any).mockResolvedValue({
      stripe_publishable_key: ""
    });
    vi.spyOn(console, "log").mockImplementation(() => {});

    await expect(
      runCli([
        "node",
        "selva",
        "buy",
        "amzn_B0TEST",
        "--method",
        "card",
        "--number",
        "4242424242424242",
        "--exp",
        "12/34",
        "--cvv",
        "123"
      ])
    ).rejects.toThrow("Card tokenization requires STRIPE_PUBLISHABLE_KEY on the API.");
  });
});


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)