feat: initialize asin-check project with Bun
- Add README.md with installation and usage instructions. - Create bun.lock for dependency management. - Add package.json to define project metadata and dependencies. - Implement caching with Redis in cache.ts for ASIN data. - Configure environment variables in config.ts for API keys and Redis URL. - Develop main application logic in index.ts to read products, fetch data, and analyze results. - Integrate Keepa API for product data retrieval in keepa.ts. - Create LLM analysis functionality in llm.ts for product viability assessment. - Implement product reading from Excel files in reader.ts. - Stub SP-API integration in sp-api.ts for future implementation. - Define TypeScript types in types.ts for product and analysis data structures. - Write results to console and CSV in writer.ts. - Configure TypeScript settings in tsconfig.json for project compilation.
This commit is contained in:
155
src/index.ts
Normal file
155
src/index.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { readProducts } from "./reader.ts";
|
||||
import { fetchKeepaDataBatch } from "./keepa.ts";
|
||||
import { fetchSpApiData } from "./sp-api.ts";
|
||||
import { connectCache, getCache, setCache, disconnectCache } from "./cache.ts";
|
||||
import { analyzeProducts } from "./llm.ts";
|
||||
import { printResults, writeResultsCsv } from "./writer.ts";
|
||||
import type { EnrichedProduct, AnalysisResult, KeepaData, ProductRecord } from "./types.ts";
|
||||
|
||||
const LLM_BATCH_SIZE = 5;
|
||||
|
||||
function parseArgs(): { inputFile: string; outputFile?: string } {
|
||||
const args = process.argv.slice(2);
|
||||
const inputFile = args.find((a) => !a.startsWith("--"));
|
||||
const outIdx = args.indexOf("--out");
|
||||
const outputFile = outIdx !== -1 ? args[outIdx + 1] : undefined;
|
||||
|
||||
if (!inputFile) {
|
||||
console.error("Usage: bun run src/index.ts <input.csv|xlsx> [--out results.csv]");
|
||||
process.exit(1);
|
||||
}
|
||||
return { inputFile, outputFile };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { inputFile, outputFile } = parseArgs();
|
||||
|
||||
console.log("Connecting to Redis...");
|
||||
await connectCache();
|
||||
|
||||
console.log(`\nReading ${inputFile}...`);
|
||||
const products = readProducts(inputFile);
|
||||
|
||||
if (products.length === 0) {
|
||||
console.error("No valid products found in input file.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Phase 1: Check cache for all ASINs
|
||||
console.log(`\nChecking cache for ${products.length} products...`);
|
||||
const cached = new Map<string, EnrichedProduct>();
|
||||
const uncachedProducts: ProductRecord[] = [];
|
||||
|
||||
for (const p of products) {
|
||||
const hit = await getCache(p.asin);
|
||||
if (hit) {
|
||||
console.log(` [cache hit] ${p.asin}`);
|
||||
cached.set(p.asin, hit);
|
||||
} else {
|
||||
uncachedProducts.push(p);
|
||||
}
|
||||
}
|
||||
console.log(`${cached.size} cached, ${uncachedProducts.length} to fetch`);
|
||||
|
||||
// Phase 2: Batch fetch from Keepa (all uncached ASINs in one request if ≤100)
|
||||
let keepaResults = new Map<string, KeepaData>();
|
||||
if (uncachedProducts.length > 0) {
|
||||
console.log(`\nFetching ${uncachedProducts.length} ASINs from Keepa...`);
|
||||
try {
|
||||
keepaResults = await fetchKeepaDataBatch(uncachedProducts.map((p) => p.asin));
|
||||
} catch (err) {
|
||||
console.warn(`Keepa batch fetch failed: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Build enriched products
|
||||
console.log(`\nEnriching products...`);
|
||||
const enriched: EnrichedProduct[] = [];
|
||||
|
||||
for (const p of products) {
|
||||
const cachedProduct = cached.get(p.asin);
|
||||
if (cachedProduct) {
|
||||
enriched.push(cachedProduct);
|
||||
continue;
|
||||
}
|
||||
|
||||
const keepa = keepaResults.get(p.asin) ?? null;
|
||||
const spApi = await fetchSpApiData(p.asin);
|
||||
|
||||
if (keepa?.currentPrice && spApi.estimatedSalePrice === 0) {
|
||||
spApi.estimatedSalePrice = keepa.currentPrice;
|
||||
}
|
||||
|
||||
const product: EnrichedProduct = {
|
||||
record: p,
|
||||
keepa,
|
||||
spApi,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await setCache(p.asin, product);
|
||||
enriched.push(product);
|
||||
|
||||
if (keepa) {
|
||||
console.log(` [enriched] ${p.asin} — price: $${keepa.currentPrice ?? "N/A"}, rank: ${keepa.salesRank ?? "N/A"}`);
|
||||
} else {
|
||||
console.log(` [no keepa] ${p.asin} — using spreadsheet data only`);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: LLM analysis in batches
|
||||
console.log(`\nAnalyzing products via LLM (batch size: ${LLM_BATCH_SIZE})...\n`);
|
||||
|
||||
const results: AnalysisResult[] = [];
|
||||
for (let i = 0; i < enriched.length; i += LLM_BATCH_SIZE) {
|
||||
const batch = enriched.slice(i, i + LLM_BATCH_SIZE);
|
||||
const batchNum = Math.floor(i / LLM_BATCH_SIZE) + 1;
|
||||
const totalBatches = Math.ceil(enriched.length / LLM_BATCH_SIZE);
|
||||
console.log(` LLM batch ${batchNum}/${totalBatches}...`);
|
||||
|
||||
// Wait between batches to avoid overwhelming LM Studio
|
||||
if (i > 0) {
|
||||
console.log(` Waiting 5s before next batch...`);
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
}
|
||||
|
||||
let verdicts;
|
||||
try {
|
||||
verdicts = await analyzeProducts(batch);
|
||||
} catch {
|
||||
console.warn(` LLM batch error, retrying after 10s...`);
|
||||
await new Promise((r) => setTimeout(r, 10_000));
|
||||
try {
|
||||
verdicts = await analyzeProducts(batch);
|
||||
} catch (retryErr) {
|
||||
console.error(` LLM analysis failed: ${retryErr}`);
|
||||
verdicts = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 0; j < batch.length; j++) {
|
||||
results.push({
|
||||
product: batch[j]!,
|
||||
verdict: verdicts?.[j] ?? {
|
||||
asin: batch[j]!.record.asin,
|
||||
verdict: "SKIP",
|
||||
confidence: 0,
|
||||
reasoning: "LLM analysis failed",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
printResults(results);
|
||||
|
||||
if (outputFile) {
|
||||
writeResultsCsv(results, outputFile);
|
||||
}
|
||||
|
||||
await disconnectCache();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user