diff --git a/.abacusai/config.json b/.abacusai/config.json index 60b41cb..5d566ec 100644 --- a/.abacusai/config.json +++ b/.abacusai/config.json @@ -10,7 +10,15 @@ "KillShell", "Bash(bunx *)", "Bash(git *)", - "Bash(ls *)" + "Bash(ls *)", + "Bash(bun run build:web 2>&1 || true)", + "Bash(bun run build:web 2>&1 || true)", + "Bash(bun run build:web 2>&1 || true)", + "Bash(bun run build:web 2>&1 || true)", + "Bash(bun run build:web 2>&1 || true)" ] - } + }, + "additionalDirectories": [ + "/Users/nvictor/.abacusai/tmp/codellm-prompt-djc6Bc" + ] } \ No newline at end of file diff --git a/src/categories/bestsellers-by-category.ts b/src/categories/bestsellers-by-category.ts index 0fd63e3..0991228 100644 --- a/src/categories/bestsellers-by-category.ts +++ b/src/categories/bestsellers-by-category.ts @@ -510,7 +510,7 @@ async function discoverCategories( maxCategories: number, ): Promise { const data = await keepaGetJson( - `/category?key=${encodeURIComponent(config.keepaApiKey)}&domain=${DOMAIN_US}&category=0`, + `/category?key=${encodeURIComponent(config.keepaApiKey)}&domain=${DOMAIN_US}&category=0&parents=0`, ); const categories = normalizeCategoryList(data); diff --git a/src/categories/mid-range-sellers-by-category.ts b/src/categories/mid-range-sellers-by-category.ts index 2123b44..f3db3da 100644 --- a/src/categories/mid-range-sellers-by-category.ts +++ b/src/categories/mid-range-sellers-by-category.ts @@ -845,7 +845,7 @@ async function discoverCategories( maxCategories: number, ): Promise { const data = await keepaGetJson( - `/category?key=${encodeURIComponent(config.keepaApiKey)}&domain=${DOMAIN_US}&category=0`, + `/category?key=${encodeURIComponent(config.keepaApiKey)}&domain=${DOMAIN_US}&category=0&parents=0`, ); const categories = normalizeCategoryList(data); diff --git a/src/categories/top-monthly-sold-by-category.ts b/src/categories/top-monthly-sold-by-category.ts index 41fd862..236918c 100644 --- a/src/categories/top-monthly-sold-by-category.ts +++ b/src/categories/top-monthly-sold-by-category.ts @@ -542,7 +542,7 @@ async function discoverCategories( maxCategories: number, ): Promise { const data = await keepaGetJson( - `/category?key=${encodeURIComponent(config.keepaApiKey)}&domain=${DOMAIN_US}&category=0`, + `/category?key=${encodeURIComponent(config.keepaApiKey)}&domain=${DOMAIN_US}&category=0&parents=0`, ); const categories = normalizeCategoryList(data); diff --git a/src/integrations/keepa.test.ts b/src/integrations/keepa.test.ts index 0194980..bd3890a 100644 --- a/src/integrations/keepa.test.ts +++ b/src/integrations/keepa.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeEach, expect, mock, test } from "bun:test"; -import { lookupKeepaUpcs, mapUpcsToAsins } from "./keepa.ts"; +import { fetchKeepaDataBatch, lookupKeepaUpcs, mapUpcsToAsins } from "./keepa.ts"; const originalFetch = globalThis.fetch; @@ -215,6 +215,7 @@ test("lookupKeepaUpcs uses lightweight query params for code mapping", async () expect(url.searchParams.has("stats")).toBe(false); expect(url.searchParams.has("buybox")).toBe(false); expect(url.searchParams.has("days")).toBe(false); + expect(url.searchParams.get("history")).toBe("0"); return new Response( JSON.stringify({ @@ -240,3 +241,49 @@ test("lookupKeepaUpcs uses lightweight query params for code mapping", async () expect(details.get(targetUpc)?.status).toBe("found"); expect(details.get(targetUpc)?.asin).toBe("B000LGT001"); }); + +test("fetchKeepaDataBatch uses token-efficient params", async () => { + const targetAsin = "B000EFF001"; + const fetchMock = mock(async (input: string | URL | Request) => { + const rawUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; + + const url = new URL(rawUrl); + expect(url.searchParams.get("asin")).toBe(targetAsin); + expect(url.searchParams.get("stats")).toBe("90"); + expect(url.searchParams.get("days")).toBe("90"); + expect(url.searchParams.get("history")).toBe("0"); + expect(url.searchParams.has("buybox")).toBe(false); + + return new Response( + JSON.stringify({ + products: [ + { + asin: targetAsin, + stats: { + current: [1999, null, null, 1234, null, null, null, null, null, null, null, 8], + avg: [2099, null, null, 1300], + min: [1799], + max: [2299], + }, + csv: [[1, 1999]], + }, + ], + tokensLeft: 9, + refillRate: 21, + }), + { status: 200 }, + ); + }); + + globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch; + + const details = await fetchKeepaDataBatch([targetAsin]); + + expect(fetchMock.mock.calls.length).toBe(1); + expect(details.get(targetAsin)?.currentPrice).toBe(19.99); +}); diff --git a/src/integrations/keepa.ts b/src/integrations/keepa.ts index 128d8a3..0ea1cc3 100644 --- a/src/integrations/keepa.ts +++ b/src/integrations/keepa.ts @@ -57,11 +57,13 @@ function buildProductUrl( options?: { includeStats?: boolean; includeBuybox?: boolean; + includeHistory?: boolean; days?: number; }, ): string { const includeStats = options?.includeStats ?? true; const includeBuybox = options?.includeBuybox ?? true; + const includeHistory = options?.includeHistory ?? true; const days = options?.days ?? 90; const params = new URLSearchParams({ @@ -78,6 +80,10 @@ function buildProductUrl( params.set("buybox", "1"); } + if (!includeHistory) { + params.set("history", "0"); + } + params.set(queryParam, values.join(",")); return `${KEEPA_BASE}/product?${params.toString()}`; } @@ -242,7 +248,8 @@ export async function fetchKeepaDataBatch( const chunk = canonicalAsins.slice(i, i + MAX_ASINS_PER_REQUEST); const url = buildProductUrl("asin", chunk, { includeStats: true, - includeBuybox: true, + includeBuybox: false, + includeHistory: false, days: 90, }); @@ -302,6 +309,7 @@ export async function lookupKeepaUpcs( const url = buildProductUrl("code", chunk, { includeStats: false, includeBuybox: false, + includeHistory: false, }); console.log( diff --git a/src/stalker/stalker-sellability.test.ts b/src/stalker/stalker-sellability.test.ts index b5e5b48..810b3c8 100644 --- a/src/stalker/stalker-sellability.test.ts +++ b/src/stalker/stalker-sellability.test.ts @@ -194,7 +194,6 @@ test("sellability checks matched seller inventory, not the source ASIN", async ( { input: inputPath, maxAsins: null, - storefrontUpdateHours: 168, offerLimit: 20, sellerLimit: 30, inventoryLimit: 200, diff --git a/src/stalker/stalker.test.ts b/src/stalker/stalker.test.ts index 9068f8d..5aff328 100644 --- a/src/stalker/stalker.test.ts +++ b/src/stalker/stalker.test.ts @@ -198,7 +198,7 @@ test("runStalker fetches product offers, filters sellers, and tracks stats", asy if (url.pathname === "/seller") { const wantsStorefront = url.searchParams.get("storefront") === "1"; if (wantsStorefront) { - expect(url.searchParams.get("update")).toBe("168"); + expect(url.searchParams.has("update")).toBeFalse(); } const sellerId = url.searchParams.get("seller"); @@ -244,7 +244,6 @@ test("runStalker fetches product offers, filters sellers, and tracks stats", asy const stats = await runStalker({ input: inputPath, maxAsins: null, - storefrontUpdateHours: 168, offerLimit: 20, sellerLimit: 30, inventoryLimit: 200, diff --git a/src/stalker/stalker.ts b/src/stalker/stalker.ts index 69713f7..3756f83 100644 --- a/src/stalker/stalker.ts +++ b/src/stalker/stalker.ts @@ -20,7 +20,6 @@ import type { SellabilityInfo } from "../types.ts"; const KEEPA_BASE = "https://api.keepa.com"; const DOMAIN_US = "1"; const AMAZON_US_SELLER_ID = "ATVPDKIKX0DER"; -const DEFAULT_STOREFRONT_UPDATE_HOURS = 168; const DEFAULT_OFFER_LIMIT = 100; const DEFAULT_SELLER_LIMIT = 30; const DEFAULT_INVENTORY_LIMIT = 200; @@ -41,7 +40,6 @@ export type StalkerArgs = { input: string; dbPath?: string; maxAsins: number | null; - storefrontUpdateHours: number; offerLimit: number; sellerLimit: number; inventoryLimit: number; @@ -142,7 +140,6 @@ export function parseArgs(argv = process.argv.slice(2)): StalkerArgs { } const maxAsinsRaw = readFlagValue(argv, "--max-asins"); - const storefrontUpdateRaw = readFlagValue(argv, "--storefront-update-hours"); const offerLimitRaw = readFlagValue(argv, "--offer-limit"); const sellerLimitRaw = readFlagValue(argv, "--seller-limit"); const inventoryLimitRaw = readFlagValue(argv, "--inventory-limit"); @@ -150,9 +147,6 @@ export function parseArgs(argv = process.argv.slice(2)): StalkerArgs { const maxSellerRequestsRaw = readFlagValue(argv, "--max-seller-requests"); const maxAsins = maxAsinsRaw ? Number(maxAsinsRaw) : null; - const storefrontUpdateHours = storefrontUpdateRaw - ? Number(storefrontUpdateRaw) - : DEFAULT_STOREFRONT_UPDATE_HOURS; const offerLimit = offerLimitRaw ? Number(offerLimitRaw) : DEFAULT_OFFER_LIMIT; @@ -183,12 +177,6 @@ export function parseArgs(argv = process.argv.slice(2)): StalkerArgs { printUsageAndExit("--max-asins must be a positive integer."); } - if (!Number.isInteger(storefrontUpdateHours) || storefrontUpdateHours < 0) { - printUsageAndExit( - "--storefront-update-hours must be a non-negative integer.", - ); - } - if (!Number.isInteger(offerLimit) || offerLimit < 20 || offerLimit > 100) { printUsageAndExit("--offer-limit must be an integer from 20 to 100."); } @@ -215,7 +203,6 @@ export function parseArgs(argv = process.argv.slice(2)): StalkerArgs { return { input, maxAsins, - storefrontUpdateHours, offerLimit, sellerLimit, inventoryLimit, @@ -662,7 +649,6 @@ async function fetchKeepaInventoryProductDetails( asin: chunk.join(","), stats: "30", days: "30", - buybox: "1", }); const data = await fetchKeepaWithRetries( @@ -746,13 +732,15 @@ async function fetchQualifiedSellerStorefronts( for (const sellerId of uniqueSellerIds) { const cached = context.storefrontCache.get(sellerId) ?? + context.metadataCache.get(sellerId) ?? (await loadCachedSeller( sellerId, args.sellerCacheHours, true, args.inventoryLimit, )); - if (cached) { + if (cached && cached.storefrontAsinTotal > 0) { + context.metadataCache.set(sellerId, cached); context.storefrontCache.set(sellerId, cached); out.set(sellerId, cached); continue; @@ -765,7 +753,6 @@ async function fetchQualifiedSellerStorefronts( domain: DOMAIN_US, seller: sellerId, storefront: "1", - update: String(args.storefrontUpdateHours), }); context.stats.sellerStorefrontRequests += 1; @@ -1566,7 +1553,7 @@ function hasFlag(args: string[], flag: string): boolean { function printUsageAndExit(message: string): never { console.error(message); console.error( - "Usage: bun run stalker --input input/asins.xlsx [--db db/results.db] [--max-asins N] [--offer-limit 100] [--seller-limit 30] [--inventory-limit 200] [--storefront-update-hours 168] [--seller-cache-hours 168] [--max-seller-requests N] [--sellability] [--analyze-sellable] [--include-stock] [--dry-run] [--no-resume] [--claude]", + "Usage: bun run stalker --input input/asins.xlsx [--db db/results.db] [--max-asins N] [--offer-limit 100] [--seller-limit 30] [--inventory-limit 200] [--seller-cache-hours 168] [--max-seller-requests N] [--sellability] [--analyze-sellable] [--include-stock] [--dry-run] [--no-resume] [--claude]", ); process.exit(1); }