@@ -2,14 +2,20 @@ import index from "./web/index.html";
import * as XLSX from "xlsx" ;
import { normalizeAsin } from "./asin.ts" ;
import { db , client } from "./db/index.ts" ;
import { analysisRevisions } from "./db/schema.ts " ;
import { eq } from "drizzle-orm " ;
import { analysisRevisions , productDistributorResearch } from "./db/schema.ts" ;
import { insertObservation , refreshRunStats } from "./db/persistence.ts" ;
import { config } from "./config.ts" ;
import {
fetchKeepaDataBatch ,
lookupKeepaUpcs ,
mapUpcsToAsins ,
} from "./integrations/keepa.ts" ;
import { analyzeProducts } from "./integrations/llm.ts" ;
import {
searchAsinOffers ,
type SearxngOfferSearchResult ,
} from "./integrations/searxng.ts" ;
import {
fetchSellabilityBatch ,
fetchSpApiPricingAndFees ,
@@ -37,7 +43,10 @@ async function pgGet<T extends Record<string, unknown>>(
query : string ,
params : unknown [ ] = [ ] ,
) : Promise < T | null > {
const rows = await client . unsafe < T [ ] > ( toPostgresSql ( query ) , params as never [ ] ) ;
const rows = await client . unsafe < T [ ] > (
toPostgresSql ( query ) ,
params as never [ ] ,
) ;
return ( rows [ 0 ] as T ) ? ? null ;
}
@@ -45,7 +54,10 @@ async function pgAll<T extends Record<string, unknown>>(
query : string ,
params : unknown [ ] = [ ] ,
) : Promise < T [ ] > {
return client . unsafe < T [ ] > ( toPostgresSql ( query ) , params as never [ ] ) as unknown as T [ ] ;
return client . unsafe < T [ ] > (
toPostgresSql ( query ) ,
params as never [ ] ,
) as unknown as T [ ] ;
}
async function pgRun ( query : string , params : unknown [ ] = [ ] ) : Promise < number > {
@@ -118,7 +130,10 @@ function safeSort(
}
function splitRawUpcValues ( input : string ) : string [ ] {
return input . split ( /[\s,;|]+/ ) . map ( ( v ) = > v . trim ( ) ) . filter ( Boolean ) ;
return input
. split ( /[\s,;|]+/ )
. map ( ( v ) = > v . trim ( ) )
. filter ( Boolean ) ;
}
function collectUpcs ( value : unknown , target : string [ ] ) : void {
@@ -279,7 +294,13 @@ async function getRuns(filters: URLSearchParams) {
[ . . . params , pageSize , offset ] ,
) ;
const total = Number ( totalRow ? . total ? ? 0 ) ;
return { items , page , pageSize , total , totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) } ;
return {
items ,
page ,
pageSize ,
total ,
totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) ,
} ;
}
async function getRun ( runId : number ) {
@@ -403,7 +424,11 @@ const ITEM_SORTS: Record<string, string> = {
async function getRunItems ( runId : number , filters : URLSearchParams ) {
const { page , pageSize , offset } = pageInput ( filters ) ;
const { where , params } = itemFilters ( filters , runId ) ;
const orderBy = safeSort ( filters . get ( "sort" ) , ITEM_SORTS , "monthly_sold DESC NULLS LAST, asin ASC" ) ;
const orderBy = safeSort (
filters . get ( "sort" ) ,
ITEM_SORTS ,
"monthly_sold DESC NULLS LAST, asin ASC" ,
) ;
const totalRow = await pgGet < { total : string } > (
` SELECT COUNT(*) AS total FROM ( ${ ITEM_ROWS } ) item_rows ${ where } ` ,
params ,
@@ -413,29 +438,60 @@ async function getRunItems(runId: number, filters: URLSearchParams) {
[ . . . params , pageSize , offset ] ,
) ;
const total = Number ( totalRow ? . total ? ? 0 ) ;
return { items , page , pageSize , total , totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) } ;
return {
items ,
page ,
pageSize ,
total ,
totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) ,
} ;
}
async function exportRunItems ( runId : number , filters : URLSearchParams ) {
const { where , params } = itemFilters ( filters , runId ) ;
const orderBy = safeSort ( filters . get ( "sort" ) , ITEM_SORTS , "monthly_sold DESC NULLS LAST, asin ASC" ) ;
const orderBy = safeSort (
filters . get ( "sort" ) ,
ITEM_SORTS ,
"monthly_sold DESC NULLS LAST, asin ASC" ,
) ;
const rows = await pgAll < Record < string , unknown > > (
` SELECT * FROM ( ${ ITEM_ROWS } ) item_rows ${ where } ORDER BY ${ orderBy } ` ,
params ,
) ;
const headers = [
"run_id" , "asin" , "product_name" , "brand" , "category" , "unit_cost" ,
"current_price" , "avg_price_90d" , "sales_rank_avg_90d" , "seller_count " ,
"amazon_is_seller" , "amazon_buybox_share_pct_90d" , "monthly_sold " ,
"sellability_status" , "verdict" , "confidence" , "reasoning" , "fetched_at " ,
"run_id" ,
"asin " ,
"product_name " ,
"brand " ,
"category" ,
"unit_cost" ,
"current_price" ,
"avg_price_90d" ,
"sales_rank_avg_90d" ,
"seller_count" ,
"amazon_is_seller" ,
"amazon_buybox_share_pct_90d" ,
"monthly_sold" ,
"sellability_status" ,
"verdict" ,
"confidence" ,
"reasoning" ,
"fetched_at" ,
] ;
return [ headers . join ( "," ) , . . . rows . map ( ( row ) = > headers . map ( ( h ) = > escapeCsvValue ( row [ h ] ) ) . join ( "," ) ) ] . join ( "\n" ) ;
return [
headers . join ( "," ) ,
. . . rows . map ( ( row ) = > headers . map ( ( h ) = > escapeCsvValue ( row [ h ] ) ) . join ( "," ) ) ,
] . join ( "\n" ) ;
}
async function getProducts ( filters : URLSearchParams ) {
const { page , pageSize , offset } = pageInput ( filters ) ;
const { where , params } = itemFilters ( filters ) ;
const orderBy = safeSort ( filters . get ( "sort" ) , ITEM_SORTS , "fetched_at DESC NULLS LAST, asin ASC" ) ;
const orderBy = safeSort (
filters . get ( "sort" ) ,
ITEM_SORTS ,
"fetched_at DESC NULLS LAST, asin ASC" ,
) ;
const base = `
SELECT product.asin, product.asin AS product_asin,
latest.item_id, latest.run_id AS "runId", latest.process_type AS "processType",
@@ -457,23 +513,28 @@ async function getProducts(filters: URLSearchParams) {
LIMIT 1
) latest ON TRUE ` ;
const total = Number (
( await pgGet < { total : string } > (
(
await pgGet < { total : string } > (
` SELECT COUNT(*) AS total FROM ( ${ base } ) products ${ where } ` ,
params ,
)) ? . total ? ? 0 ,
)
) ? . total ? ? 0 ,
) ;
const items = await pgAll (
` SELECT * FROM ( ${ base } ) products ${ where } ORDER BY ${ orderBy } LIMIT ? OFFSET ? ` ,
[ . . . params , pageSize , offset ] ,
) ;
return { items , page , pageSize , total , totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) } ;
return {
items ,
page ,
pageSize ,
total ,
totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) ,
} ;
}
async function getProduct ( asin : string ) {
const product = await pgGet (
` SELECT * FROM products WHERE asin = ? ` ,
[ asin ] ,
) ;
const product = await pgGet ( ` SELECT * FROM products WHERE asin = ? ` , [ asin ] ) ;
if ( ! product ) return null ;
const observations = await pgAll (
` SELECT observation.*, run.type AS run_type
@@ -492,10 +553,71 @@ async function getProduct(asin: string) {
ORDER BY revision.analyzed_at DESC ` ,
[ asin ] ,
) ;
return { product , observations , analyses } ;
const distributorResearchRows = await pgAll < Record < string , unknown > > (
` SELECT id, run_item_id, inventory_item_id, provider, model, status, distributors_json, raw_response, created_at
FROM product_distributor_research
WHERE product_asin = ?
ORDER BY created_at DESC, id DESC ` ,
[ asin ] ,
) ;
const distributorResearch = distributorResearchRows . map ( ( row ) = > {
const distributors = ( ( ) = > {
try {
return normalizeDistributorCandidates (
JSON . parse ( String ( row . distributors_json ? ? "[]" ) ) ,
) ;
} catch {
return [ ] ;
}
} ) ( ) ;
return {
id : Number ( row . id ) ,
run_item_id : row.run_item_id == null ? null : Number ( row . run_item_id ) ,
inventory_item_id :
row.inventory_item_id == null ? null : Number ( row . inventory_item_id ) ,
provider : String ( row . provider ? ? "" ) ,
model : String ( row . model ? ? "" ) ,
status : String ( row . status ? ? "" ) ,
created_at : String ( row . created_at ? ? "" ) ,
distributors ,
raw_response : row.raw_response == null ? null : String ( row . raw_response ) ,
} ;
} ) ;
return { product , observations , analyses , distributorResearch } ;
}
async function reanalyzeRunItem ( itemId : number ) {
async function findLatestRunItemIdByAsin ( asin : string ) : Promise < number | null > {
const row = await pgGet < { id : number } > (
` SELECT ri.id
FROM run_items ri
WHERE ri.product_asin = ?
ORDER BY ri.id DESC
LIMIT 1 ` ,
[ asin ] ,
) ;
return row ? . id == null ? null : Number ( row . id ) ;
}
async function reanalyzeStalkerProductByAsin (
asin : string ,
useClaude = USE_CLAUDE ,
) {
const runItemId = await findLatestRunItemIdByAsin ( asin ) ;
if ( runItemId == null ) {
throw new Error ( "Stalker product item not found" ) ;
}
return reanalyzeRunItem ( runItemId , useClaude ) ;
}
async function findDistributorsForStalkerProductByAsin ( asin : string ) {
const runItemId = await findLatestRunItemIdByAsin ( asin ) ;
if ( runItemId == null ) {
throw new Error ( "Stalker product item not found" ) ;
}
return findDistributorsForStalkerProduct ( runItemId ) ;
}
async function reanalyzeRunItem ( itemId : number , useClaude = USE_CLAUDE ) {
const row = await pgGet < Record < string , any > > (
` SELECT ri.id, ri.run_id, ri.product_asin AS asin, r.type,
COALESCE(p.name, si.supplied_name, ri.product_asin) AS product_name,
@@ -505,7 +627,8 @@ async function reanalyzeRunItem(itemId: number) {
si.fba_net_sheet, si.gross_profit_dollar, si.gross_profit_pct,
si.net_profit_sheet, si.roi_sheet, si.moq, si.moq_cost,
si.qty_available, si.supplier, si.source_url, si.asin_link,
si.promo_coupon_code, si.notes, si.lead_date
si.promo_coupon_code, si.notes, si.lead_date,
ri.source_inventory_item_id
FROM run_items ri JOIN runs r ON r.id = ri.run_id
LEFT JOIN products p ON p.asin = ri.product_asin
LEFT JOIN sourcing_inputs si ON si.run_item_id = ri.id
@@ -514,7 +637,9 @@ async function reanalyzeRunItem(itemId: number) {
) ;
if ( ! row ) throw new Error ( "Run item not found" ) ;
if ( row . type === "supplier_upc" ) {
throw new Error ( "Supplier scoring revisions are produced by the supplier pipeline" ) ;
throw new Error (
"Supplier scoring revisions are produced by the supplier pipeline" ,
) ;
}
const record : ProductRecord = {
asin : row.asin ,
@@ -555,15 +680,18 @@ async function reanalyzeRunItem(itemId: number) {
spApi : spApi as SpApiData ,
fetchedAt : new Date ( ) . toISOString ( ) ,
} ;
const verdict =
( await analyzeProducts ( [ enriched ] , { useClaude : USE_CLAUDE } ) ) [ 0 ] ? ? {
const verdict = ( await analyzeProducts ( [ enriched ] , { useClaude } ) ) [ 0 ] ? ? {
asin : row.asin ,
verdict : "SKIP" as const ,
confidence : 0 ,
reasoning : "LLM analysis returned no verdict" ,
} ;
const result : AnalysisResult = { product : enriched , verdict } ;
const observationId = await insertObservation ( row . run_id , result , "reanalysis" ) ;
const observationId = await insertObservation (
row . run_id ,
result ,
"reanalysis" ,
) ;
await db . insert ( analysisRevisions ) . values ( {
runItemId : itemId ,
observationId ,
@@ -574,7 +702,244 @@ async function reanalyzeRunItem(itemId: number) {
analyzedAt : new Date ( enriched . fetchedAt ) ,
} ) ;
await refreshRunStats ( row . run_id ) ;
return { itemId , runId : row.run_id , asin : row.asin , fetchedAt : enriched.fetchedAt } ;
return {
itemId ,
runId : row.run_id ,
asin : row.asin ,
fetchedAt : enriched.fetchedAt ,
} ;
}
type DistributorCandidate = {
name : string ;
website : string ;
rationale : string ;
confidence : number ;
reputation : string ;
contactInfo : string ;
outreachDraft : string ;
} ;
function clampDistributorConfidence ( value : unknown ) : number {
const parsed = Number ( value ) ;
if ( ! Number . isFinite ( parsed ) ) return 0 ;
return Math . max ( 0 , Math . min ( 100 , Math . round ( parsed ) ) ) ;
}
function normalizeDistributorCandidates (
payload : unknown ,
) : DistributorCandidate [ ] {
if ( ! Array . isArray ( payload ) ) return [ ] ;
return payload
. filter (
( item ) : item is Record < string , unknown > = >
item != null && typeof item === "object" ,
)
. map ( ( item ) = > ( {
name : String ( item . name ? ? "" ) . trim ( ) ,
website : String ( item . website ? ? "" ) . trim ( ) ,
rationale : String ( item . rationale ? ? "" ) . trim ( ) ,
confidence : clampDistributorConfidence ( item . confidence ) ,
reputation : String ( item . reputation ? ? "" ) . trim ( ) ,
contactInfo : String ( item . contact_info ? ? item . contactInfo ? ? "" ) . trim ( ) ,
outreachDraft : String (
item . outreach_draft ? ? item . outreachDraft ? ? "" ,
) . trim ( ) ,
} ) )
. filter ( ( item ) = > item . name . length > 0 && item . website . length > 0 )
. slice ( 0 , 10 ) ;
}
function extractJsonArrayFromText ( text : string ) : string {
const trimmed = text . trim ( ) ;
const fence = trimmed . match ( /```(?:json)?\s*([\s\S]*?)```/i ) ;
const candidate = fence ? ( fence [ 1 ] ? . trim ( ) ? ? "" ) : trimmed ;
const start = candidate . indexOf ( "[" ) ;
const end = candidate . lastIndexOf ( "]" ) ;
if ( start >= 0 && end > start ) {
return candidate . slice ( start , end + 1 ) ;
}
return candidate ;
}
async function requestClaudeDistributorCandidates (
context : Record < string , unknown > ,
) {
if ( ! config . anthropicApiKey ) {
throw new Error ( "Missing required env var: ANTHROPIC_API_KEY" ) ;
}
// const model = (config.anthropicModel ?? "claude-sonnet-4-6").trim() || "claude-sonnet-4-6";
const model = "claude-sonnet-4-6" ;
const system = [
"You are a wholesale sourcing researcher who identifies authorized U.S. distributors for Amazon products." ,
"For each candidate you research their reputation, locate real point-of-contact details, and draft a concise cold-outreach message." ,
"Return only raw JSON — no prose, no markdown fences." ,
] . join ( " " ) ;
const prompt = [
"Analyze the Amazon product context below and identify up to 5 likely authorized U.S. wholesale distributors." ,
"" ,
"For each distributor:" ,
"1. Identify whether they are an official brand distributor, authorized reseller, or national wholesaler." ,
"2. Investigate their reputation: check for BBB accreditation, industry tenure, any known complaints or red flags, and whether they appear on the brand's own authorized-distributor list." ,
"3. Find the most direct point-of-contact for opening a new wholesale account. Search the distributor's website for a dedicated wholesale, reseller, or new-account page. Return AS MANY of these as you can find: full name and title of the wholesale/vendor relations contact, direct email address (e.g. wholesale@..., newaccounts@..., sales@...), direct phone number, and the URL of the wholesale application or inquiry page. If a named contact is not publicly listed, return the best department email and phone. Do NOT return a generic contact form URL as the only answer." ,
"4. Draft a short, professional cold-outreach message (3– 5 sentences) I can copy-paste and send. Tone: warm, genuine, and business-oriented — the goal is to start a relationship, not close a deal. Rules: (a) Praise the brand's reputation, quality, or market position sincerely — make it specific to what this brand is known for. (b) Frame the inquiry as a mutual growth opportunity; express eagerness to carry their line and help it reach more customers. (c) Do NOT mention Amazon, FBA, or online marketplaces anywhere in the message — present yourself simply as a retailer / reseller interested in carrying their products. (d) Ask about wholesale account requirements and invite them to share terms or an application. (e) Keep it concise and human — avoid corporate filler phrases." ,
"" ,
"Return a raw JSON array. Each object must have exactly these keys:" ,
' "name" — distributor company name' ,
' "website" — full URL (https://...)' ,
' "rationale" — why this distributor is a strong candidate (1– 2 sentences)' ,
' "confidence" — integer 0– 100 reflecting how confident you are this is a real authorized source' ,
' "reputation" — summary of reputation findings (BBB status, years in business, any red flags)' ,
' "contact_info" — structured string with all contact details found: "Name: ..., Title: ..., Email: ..., Phone: ..., Wholesale page: ..."' ,
' "outreach_draft"— complete ready-to-send message addressed to the specific contact' ,
"" ,
"Product context:" ,
JSON . stringify ( context , null , 2 ) ,
] . join ( "\n" ) ;
const response = await fetch ( "https://api.anthropic.com/v1/messages" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
"x-api-key" : config . anthropicApiKey ,
"anthropic-version" : "2023-06-01" ,
} ,
body : JSON.stringify ( {
model ,
system ,
messages : [ { role : "user" , content : prompt } ] ,
temperature : 0.2 ,
max_tokens : 4096 ,
} ) ,
} ) ;
const raw = await response . text ( ) ;
if ( ! response . ok ) {
throw new Error (
` Claude API error ${ response . status } : ${ raw . slice ( 0 , 300 ) } ` ,
) ;
}
let contentText = "" ;
try {
const parsed = JSON . parse ( raw ) as {
content? : Array < { type ? : string ; text? : string } > ;
} ;
contentText = ( parsed . content ? ? [ ] )
. filter (
( block ) = > block ? . type === "text" && typeof block . text === "string" ,
)
. map ( ( block ) = > block . text ? ? "" )
. join ( "\n" ) ;
} catch {
contentText = raw ;
}
const arrayText = extractJsonArrayFromText ( contentText ) ;
let candidates : DistributorCandidate [ ] = [ ] ;
try {
candidates = normalizeDistributorCandidates ( JSON . parse ( arrayText ) ) ;
} catch {
candidates = [ ] ;
}
return { model , rawResponse : contentText , candidates } ;
}
async function findDistributorsForStalkerProduct ( runItemId : number ) {
const row = await pgGet < Record < string , any > > (
` SELECT ri.id AS run_item_id, ri.product_asin AS asin, ri.source_inventory_item_id,
p.name AS product_title, p.brand, p.category,
observation.current_price, observation.avg_price_90d, observation.sales_rank,
observation.monthly_sold, observation.seller_count, observation.amazon_is_seller,
observation.can_sell, observation.sellability_status, observation.sellability_reason,
latest_analysis.decision AS verdict, latest_analysis.confidence, latest_analysis.reasoning,
seller.seller_id, seller.seller_name, seller.rating, seller.rating_count
FROM run_items ri
JOIN products p ON p.asin = ri.product_asin
LEFT JOIN product_observations observation ON observation.id = (
SELECT obs.id
FROM product_observations obs
WHERE obs.product_asin = ri.product_asin
ORDER BY obs.fetched_at DESC, obs.id DESC
LIMIT 1
)
LEFT JOIN LATERAL (
SELECT revision.decision, revision.confidence, revision.reasoning
FROM analysis_revisions revision
WHERE revision.run_item_id = ri.id
ORDER BY revision.analyzed_at DESC, revision.id DESC
LIMIT 1
) latest_analysis ON TRUE
LEFT JOIN stalker_inventory_items si ON si.id = ri.source_inventory_item_id
LEFT JOIN sellers seller ON seller.seller_id = si.seller_id
WHERE ri.id = ? ` ,
[ runItemId ] ,
) ;
if ( ! row ? . asin ) {
throw new Error ( "Stalker product item not found" ) ;
}
const offerResults = await searchAsinOffers ( row . asin , {
maxResults : 12 ,
includeUnmatchedAsinResults : true ,
} ) . catch ( ( ) = > [ ] as SearxngOfferSearchResult [ ] ) ;
const promptContext = {
asin : row.asin ,
productTitle : row.product_title ? ? null ,
brand : row.brand ? ? null ,
category : row.category ? ? null ,
metrics : {
currentPrice : row.current_price ? ? null ,
avgPrice90d : row.avg_price_90d ? ? null ,
salesRank : row.sales_rank ? ? null ,
monthlySold : row.monthly_sold ? ? null ,
sellerCount : row.seller_count ? ? null ,
amazonIsSeller : row.amazon_is_seller ? ? null ,
canSell : row.can_sell ? ? null ,
sellabilityStatus : row.sellability_status ? ? null ,
sellabilityReason : row.sellability_reason ? ? null ,
verdict : row.verdict ? ? null ,
confidence : row.confidence ? ? null ,
reasoning : row.reasoning ? ? null ,
} ,
seller : {
sellerId : row.seller_id ? ? null ,
sellerName : row.seller_name ? ? null ,
rating : row.rating ? ? null ,
ratingCount : row.rating_count ? ? null ,
} ,
offerResearch : offerResults.map ( ( result ) = > ( {
title : result.title ,
url : result.url ,
domain : result.domain ,
snippet : result.snippet ,
score : result.score ,
rank : result.rank ,
} ) ) ,
} ;
const claude = await requestClaudeDistributorCandidates ( promptContext ) ;
await db
. delete ( productDistributorResearch )
. where ( eq ( productDistributorResearch . runItemId , runItemId ) ) ;
const [ saved ] = await db
. insert ( productDistributorResearch )
. values ( {
productAsin : row.asin ,
runItemId : runItemId ,
inventoryItemId : row.source_inventory_item_id ? ? null ,
provider : "claude" ,
model : claude.model ,
status : claude.candidates.length ? "completed" : "empty" ,
queryContextJson : JSON.stringify ( promptContext ) ,
distributorsJson : JSON.stringify ( claude . candidates ) ,
rawResponse : claude.rawResponse ,
} )
. returning ( {
id : productDistributorResearch.id ,
createdAt : productDistributorResearch.createdAt ,
} ) ;
return {
asin : row.asin ,
runItemId : runItemId ,
researchId : saved?.id ? ? null ,
createdAt : saved?.createdAt ? ? null ,
distributors : claude.candidates ,
} ;
}
function stalkerBaseWhere ( filters : URLSearchParams , product = false ) {
@@ -616,7 +981,10 @@ function stalkerBaseWhere(filters: URLSearchParams, product = false) {
}
}
if ( product ) {
conditions . push ( "observation.can_sell = true" , "observation.sellability_status = 'available'" ) ;
conditions . push (
"observation.can_sell = true" ,
"observation.sellability_status = 'available'" ,
) ;
const verdict = filters . get ( "verdict" ) ? . toUpperCase ( ) ;
if ( verdict === "FBA" || verdict === "FBM" || verdict === "SKIP" ) {
conditions . push ( "analysis.decision::text = ?" ) ;
@@ -686,7 +1054,14 @@ async function getStalkerResults(filters: URLSearchParams) {
} ,
"persisted_inventory_asin_count DESC, last_seen_at DESC, seller_id ASC" ,
) ;
const total = Number ( ( await pgGet < { total : string } > ( ` SELECT COUNT(*) AS total FROM ( ${ base } ) rows ` , params ) ) ? . total ? ? 0 ) ;
const total = Number (
(
await pgGet < { total : string } > (
` SELECT COUNT(*) AS total FROM ( ${ base } ) rows ` ,
params ,
)
) ? . total ? ? 0 ,
) ;
const items = await pgAll (
` SELECT * FROM ( ${ base } ) rows ORDER BY ${ order } LIMIT ? OFFSET ? ` ,
[ . . . params , pageSize , offset ] ,
@@ -704,14 +1079,18 @@ async function getStalkerResults(filters: URLSearchParams) {
sellers : Number ( summary ? . sellers ? ? 0 ) ,
persistedInventoryAsins : Number ( summary ? . persistedInventoryAsins ? ? 0 ) ,
} ,
page , pageSize , total , totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) ,
page ,
pageSize ,
total ,
totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) ,
} ;
}
function stalkerProductSql ( where : string ) {
return ` SELECT r.id AS "runId", r.started_at, seller.seller_id, seller.seller_name,
seller.rating, seller.rating_count, inventory.product_asin AS asin,
seller.rating, seller.rating_count, inventory.id AS inventory_item_id, inventory.product_asin AS asin,
observation.can_sell, observation.sellability_status, observation.sellability_reason,
analysis.run_item_id,
product.name AS product_title, product.brand,
CASE WHEN product.category IS NULL THEN NULL ELSE json_build_array(product.category)::text END AS category_tree,
observation.current_price, observation.avg_price_90d, observation.sales_rank,
@@ -724,7 +1103,7 @@ function stalkerProductSql(where: string) {
JOIN products product ON product.asin = inventory.product_asin
JOIN product_observations observation ON observation.id = inventory.observation_id
LEFT JOIN LATERAL (
SELECT revision.decision, revision.confidence, revision.reasoning
SELECT item.id AS run_item_id, revision.decision, revision.confidence, revision.reasoning
FROM run_items item
JOIN analysis_revisions revision ON revision.run_item_id = item.id
WHERE item.source_inventory_item_id = inventory.id
@@ -761,8 +1140,16 @@ async function stalkerProducts(filters: URLSearchParams, exportOnly = false) {
} ,
"monthly_sold DESC NULLS LAST, last_seen_at DESC, asin ASC" ,
) ;
if ( exportOnly ) return pgAll ( ` SELECT * FROM ( ${ base } ) products ORDER BY ${ order } ` , params ) ;
const total = Number ( ( await pgGet < { total : string } > ( ` SELECT COUNT(*) AS total FROM ( ${ base } ) products ` , params ) ) ? . total ? ? 0 ) ;
if ( exportOnly )
return pgAll ( ` SELECT * FROM ( ${ base } ) products ORDER BY ${ order } ` , params ) ;
const total = Number (
(
await pgGet < { total : string } > (
` SELECT COUNT(*) AS total FROM ( ${ base } ) products ` ,
params ,
)
) ? . total ? ? 0 ,
) ;
const items = await pgAll (
` SELECT * FROM ( ${ base } ) products ORDER BY ${ order } LIMIT ? OFFSET ? ` ,
[ . . . params , pageSize , offset ] ,
@@ -779,12 +1166,19 @@ async function stalkerProducts(filters: URLSearchParams, exportOnly = false) {
sellers : Number ( summary ? . sellers ? ? 0 ) ,
products : Number ( summary ? . products ? ? 0 ) ,
} ,
page , pageSize , total , totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) ,
page ,
pageSize ,
total ,
totalPages : Math.max ( 1 , Math . ceil ( total / pageSize ) ) ,
} ;
}
async function exportStalkerProducts ( filters : URLSearchParams ) : Promise < Response > {
const rows = ( await stalkerProducts ( filters , true ) ) as Array < Record < string , any > > ;
async function exportStalkerProducts (
filters : URLSearchParams ,
) : Promise < Response > {
const rows = ( await stalkerProducts ( filters , true ) ) as Array <
Record < string , any >
> ;
const data = rows . map ( ( row ) = > ( {
ASIN : row.asin ,
"Amazon URL" : ` https://amazon.com/dp/ ${ row . asin } ` ,
@@ -802,7 +1196,11 @@ async function exportStalkerProducts(filters: URLSearchParams): Promise<Response
"Run ID" : row . runId ,
} ) ) ;
const workbook = XLSX . utils . book_new ( ) ;
XLSX . utils . book_append_sheet ( workbook , XLSX . utils . json_to_sheet ( data ) , "Sellable Products" ) ;
XLSX . utils . book_append_sheet (
workbook ,
XLSX . utils . json_to_sheet ( data ) ,
"Sellable Products" ,
) ;
return xlsx (
XLSX . write ( workbook , { type : "array" , bookType : "xlsx" } ) as ArrayBuffer ,
"stalker-sellable-products.xlsx" ,
@@ -831,13 +1229,17 @@ const server = Bun.serve({
"/stalker" : index ,
"/stalker/products" : index ,
"/runs/:runId" : index ,
"/api/runs" : async ( req ) = > json ( await getRuns ( new URL ( req . url ) . searchParams ) ) ,
"/api/runs" : async ( req ) = >
json ( await getRuns ( new URL ( req . url ) . searchParams ) ) ,
"/api/runs/:runId" : async ( req ) = > {
const runId = Number ( req . params . runId ) ;
if ( ! Number . isInteger ( runId ) ) return json ( { error : "Invalid run identifier" } , 400 ) ;
if ( ! Number . isInteger ( runId ) )
return json ( { error : "Invalid run identifier" } , 400 ) ;
if ( req . method === "DELETE" ) {
const deleted = await pgRun ( "DELETE FROM runs WHERE id = ?" , [ runId ] ) ;
return deleted ? json ( { deletedRun : true } ) : json ( { error : "Run not found" } , 404 ) ;
return deleted
? json ( { deletedRun : true } )
: json ( { error : "Run not found" } , 404 ) ;
}
const run = await getRun ( runId ) ;
if ( ! run ) return json ( { error : "Run not found" } , 404 ) ;
@@ -855,35 +1257,123 @@ const server = Bun.serve({
} ,
"/api/runs/:runId/items" : async ( req ) = > {
const runId = Number ( req . params . runId ) ;
if ( ! Number . isInteger ( runId ) ) return json ( { error : "Invalid run identifier" } , 400 ) ;
if ( ! Number . isInteger ( runId ) )
return json ( { error : "Invalid run identifier" } , 400 ) ;
return json ( await getRunItems ( runId , new URL ( req . url ) . searchParams ) ) ;
} ,
"/api/runs/:runId/export.csv" : async ( req ) = > {
const runId = Number ( req . params . runId ) ;
if ( ! Number . isInteger ( runId ) ) return json ( { error : "Invalid run identifier" } , 400 ) ;
return csv ( await exportRunItems ( runId , new URL ( req . url ) . searchParams ) , ` run- ${ runId } .csv ` ) ;
if ( ! Number . isInteger ( runId ) )
return json ( { error : "Invalid run identifier" } , 400 ) ;
return csv (
await exportRunItems ( runId , new URL ( req . url ) . searchParams ) ,
` run- ${ runId } .csv ` ,
) ;
} ,
"/api/run-items/:itemId/reanalyze" : async ( req ) = > {
if ( req . method !== "POST" ) return json ( { error : "Method not allowed" } , 405 ) ;
if ( req . method !== "POST" )
return json ( { error : "Method not allowed" } , 405 ) ;
const itemId = Number ( req . params . itemId ) ;
if ( ! Number . isInteger ( itemId ) ) return json ( { error : "Invalid run item identifier" } , 400 ) ;
if ( ! Number . isInteger ( itemId ) )
return json ( { error : "Invalid run item identifier" } , 400 ) ;
try {
return json ( await reanalyzeRunItem ( itemId ) ) ;
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error ) ;
return json ( { error : message } , message === "Run item not found" ? 404 : 500 ) ;
return json (
{ error : message } ,
message === "Run item not found" ? 404 : 500 ,
) ;
}
} ,
"/api/products" : async ( req ) = > json ( await getProducts ( new URL ( req . url ) . searchParams ) ) ,
"/api/products" : async ( req ) = >
json ( await getProducts ( new URL ( req . url ) . searchParams ) ) ,
"/api/products/:asin" : async ( req ) = > {
const asin = normalizeAsin ( req . params . asin ) ;
if ( ! asin ) return json ( { error : "Invalid ASIN" } , 400 ) ;
const result = await getProduct ( asin ) ;
return result ? json ( result ) : json ( { error : "Product not found" } , 404 ) ;
} ,
"/api/stalker/results" : async ( req ) = > json ( await getStalkerResults ( new URL ( req . url ) . searchParams ) ) ,
"/api/stalker/products" : async ( req ) = > json ( await s talkerProduc ts( new URL ( req . url ) . searchParams ) ) ,
"/api/stalker/products/export.xlsx " : async ( req ) = > exportStalkerProducts ( new URL ( req . url ) . searchParams ) ,
"/api/stalker/results" : async ( req ) = >
json ( await getS talkerResul ts( new URL ( req . url ) . searchParams ) ) ,
"/api/stalker/products" : async ( req ) = >
json ( await stalkerProducts ( new URL ( req . url ) . searchParams ) ) ,
"/api/stalker/products/export.xlsx" : async ( req ) = >
exportStalkerProducts ( new URL ( req . url ) . searchParams ) ,
"/api/stalker/products/:runItemId/reanalyze" : async ( req ) = > {
if ( req . method !== "POST" )
return json ( { error : "Method not allowed" } , 405 ) ;
const runItemId = Number ( req . params . runItemId ) ;
if ( ! Number . isInteger ( runItemId ) )
return json ( { error : "Invalid run item identifier" } , 400 ) ;
const provider = new URL ( req . url ) . searchParams
. get ( "provider" )
? . trim ( )
. toLowerCase ( ) ;
const useClaude = provider === "claude" ;
try {
return json ( await reanalyzeRunItem ( runItemId , useClaude || USE_CLAUDE ) ) ;
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error ) ;
return json (
{ error : message } ,
message === "Run item not found" ? 404 : 500 ,
) ;
}
} ,
"/api/stalker/products/:runItemId/distributors" : async ( req ) = > {
if ( req . method !== "POST" )
return json ( { error : "Method not allowed" } , 405 ) ;
const runItemId = Number ( req . params . runItemId ) ;
if ( ! Number . isInteger ( runItemId ) )
return json ( { error : "Invalid run item identifier" } , 400 ) ;
try {
return json ( await findDistributorsForStalkerProduct ( runItemId ) ) ;
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error ) ;
return json (
{ error : message } ,
message === "Stalker product item not found" ? 404 : 500 ,
) ;
}
} ,
"/api/stalker/products/by-asin/:asin/reanalyze" : async ( req ) = > {
if ( req . method !== "POST" )
return json ( { error : "Method not allowed" } , 405 ) ;
const asin = normalizeAsin ( req . params . asin ) ;
if ( ! asin ) return json ( { error : "Invalid ASIN" } , 400 ) ;
const provider = new URL ( req . url ) . searchParams
. get ( "provider" )
? . trim ( )
. toLowerCase ( ) ;
const useClaude = provider === "claude" ;
try {
return json (
await reanalyzeStalkerProductByAsin ( asin , useClaude || USE_CLAUDE ) ,
) ;
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error ) ;
return json (
{ error : message } ,
message === "Stalker product item not found" ? 404 : 500 ,
) ;
}
} ,
"/api/stalker/products/by-asin/:asin/distributors" : async ( req ) = > {
if ( req . method !== "POST" )
return json ( { error : "Method not allowed" } , 405 ) ;
const asin = normalizeAsin ( req . params . asin ) ;
if ( ! asin ) return json ( { error : "Invalid ASIN" } , 400 ) ;
try {
return json ( await findDistributorsForStalkerProductByAsin ( asin ) ) ;
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error ) ;
return json (
{ error : message } ,
message === "Stalker product item not found" ? 404 : 500 ,
) ;
}
} ,
"/api/stalker/purge" : async ( req ) = >
req . method === "DELETE" || req . method === "POST"
? json ( await purgeStalkerData ( ) )
@@ -893,10 +1383,15 @@ const server = Bun.serve({
const upcs = await parseUpcsFromRequest ( req ) ;
const error = validateUpcs ( upcs ) ;
if ( error ) return json ( { error } , 400 ) ;
const items = [ . . . ( await mapUpcsToAsins ( upcs ) ) . entries ( ) ] . map ( ( [ upc , asin ] ) = > ( { upc , asin } ) ) ;
const items = [ . . . ( await mapUpcsToAsins ( upcs ) ) . entries ( ) ] . map (
( [ upc , asin ] ) = > ( { upc , asin } ) ,
) ;
return json ( { requested : upcs.length , matched : items.length , items } ) ;
} catch ( error ) {
return json ( { error : error instanceof Error ? error.message : String ( error ) } , 400 ) ;
return json (
{ error : error instanceof Error ? error.message : String ( error ) } ,
400 ,
) ;
}
} ,
"/api/upc/lookup" : async ( req ) = > {
@@ -905,16 +1400,31 @@ const server = Bun.serve({
const error = validateUpcs ( upcs ) ;
if ( error ) return json ( { error } , 400 ) ;
const items = [ . . . ( await lookupKeepaUpcs ( upcs ) ) . values ( ) ] ;
return json ( { requested : upcs.length , statusCounts : summarizeLookupStatuses ( items ) , items } ) ;
return json ( {
requested : upcs.length ,
statusCounts : summarizeLookupStatuses ( items ) ,
items ,
} ) ;
} catch ( error ) {
return json ( { error : error instanceof Error ? error.message : String ( error ) } , 400 ) ;
return json (
{ error : error instanceof Error ? error.message : String ( error ) } ,
400 ,
) ;
}
} ,
"/api/process/upc-file" : async ( req ) = > {
try {
return json ( await runUpcFileAnalysis ( { . . . ( await parseUpcFileRequest ( req ) ) , manageResources : false } ) ) ;
return json (
await runUpcFileAnalysis ( {
. . . ( await parseUpcFileRequest ( req ) ) ,
manageResources : false ,
} ) ,
) ;
} catch ( error ) {
return json ( { error : error instanceof Error ? error.message : String ( error ) } , 400 ) ;
return json (
{ error : error instanceof Error ? error.message : String ( error ) } ,
400 ,
) ;
}
} ,
} ,