Skip to content
Merged
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
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,18 @@ python3 scripts/generate-category-reels.py

Needs `MEDIA_UPLOAD_SECRET` + `XAI_API_KEY` (or Grok Build `~/.grok/auth.json`).
Docs: [`docs/imagine-r2-videos.md`](docs/imagine-r2-videos.md).

## Federated product library (kyasi.us)

Adazo is a **consumer** of the network product library at **https://kyasi.us**.

```bash
export KYASI_LIBRARY_TOKEN=… # kyasi-net ADMIN_API_TOKEN
npm run library:sync:dry
npm run library:sync # write-back only (catalog → library)
```

- Client: `scripts/lib/kyasi-library.mjs`
- Sync: `scripts/sync-from-library.mjs` (`imagesMode: replace`; no library→catalog image pull)
- Docs: `docs/KYASI-LIBRARY.md`
- Associate tag stays in site config / buy URLs only — never in library payloads.
27 changes: 27 additions & 0 deletions docs/KYASI-LIBRARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Adazo ↔ kyasi.us library

Adazo consumes the federated product library at **https://kyasi.us**.

## Commands

```bash
export KYASI_LIBRARY_URL=https://kyasi.us
export KYASI_LIBRARY_TOKEN=… # ADMIN_API_TOKEN from kyasi-net

npm run library:sync:dry
npm run library:sync
```

## Behavior

1. **GET** each catalog ASIN from the library (hit/miss).
2. **Write-back** house name, brand, category, slug, and **this product's** images with `imagesMode: replace`.
3. **Does not** pull library images into the TS catalog (avoids seed pollution).

## Files

| Path | Role |
|------|------|
| `scripts/lib/kyasi-library.mjs` | HTTP client |
| `scripts/sync-from-library.mjs` | Sync loop |
| `tmp/library-sync-report.json` | Last run report |
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"refresh:images": "node scripts/bsr/refresh-catalog-images.mjs",
"refresh:weekly": "npm run import:bsr && npm run build && wrangler deploy",
"deploy": "npm run build && wrangler deploy",
"content:research": "node scripts/research-product-enrichment.mjs"
"content:research": "node scripts/research-product-enrichment.mjs",
"library:sync": "node scripts/sync-from-library.mjs",
"library:sync:dry": "node scripts/sync-from-library.mjs --dry-run"
},
"dependencies": {
"lucide-react": "^1.25.0",
Expand Down
142 changes: 142 additions & 0 deletions scripts/lib/kyasi-library.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Client for the federated product library at kyasi.us
* Reads are public. Writes need KYASI_LIBRARY_TOKEN (ADMIN_API_TOKEN).
*/
const DEFAULT_BASE =
process.env.KYASI_LIBRARY_URL || "https://kyasi.us";

export function libraryBase() {
return DEFAULT_BASE.replace(/\/+$/, "");
}

export function libraryToken() {
return (
process.env.KYASI_LIBRARY_TOKEN ||
process.env.ADMIN_API_TOKEN ||
""
).trim();
}

async function req(path, opts = {}) {
const url = `${libraryBase()}${path}`;
const headers = {
Accept: "application/json",
...(opts.headers || {}),
};
if (opts.json) {
headers["Content-Type"] = "application/json";
}
const token = libraryToken();
if (opts.auth) {
if (!token) {
throw new Error(
"KYASI_LIBRARY_TOKEN (or ADMIN_API_TOKEN) required for library writes",
);
}
headers.Authorization = `Bearer ${token}`;
}
const res = await fetch(url, {
method: opts.method || "GET",
headers,
body: opts.json ? JSON.stringify(opts.json) : undefined,
});
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = { raw: text };
}
if (!res.ok) {
const msg =
(data && (data.error || data.message)) ||
`${res.status} ${res.statusText}`;
const err = new Error(`kyasi library ${opts.method || "GET"} ${path}: ${msg}`);
err.status = res.status;
err.data = data;
throw err;
}
return data;
}

/** Prefer real gallery CDN URLs over fragile images/P/{ASIN} pattern */
export function isGoodAmazonImage(url) {
if (!url || typeof url !== "string") return false;
if (/images\/P\//i.test(url)) return false;
return /media-amazon\.com\/images\/I\//i.test(url) ||
/ssl-images-amazon\.com\/images\//i.test(url);
}

export function pickPrimaryImage(images, fallback) {
const list = Array.isArray(images) ? images : [];
const good = list.find(isGoodAmazonImage);
if (good) return good;
if (isGoodAmazonImage(fallback)) return fallback;
return good || fallback || list[0] || null;
}

export async function getAmazonItem(asin) {
if (!asin) return null;
try {
const data = await req(
`/api/library/items/amazon/${encodeURIComponent(asin.toUpperCase())}`,
);
return data.item || null;
} catch (e) {
if (e.status === 404) return null;
throw e;
}
}

export async function listSiteItems(siteId, limit = 100) {
const data = await req(
`/api/library/items?site=${encodeURIComponent(siteId)}&limit=${limit}`,
);
return data;
}

export async function upsertAmazonItem(input) {
return req("/api/library/items", {
method: "POST",
auth: true,
json: {
networkId: "amazon",
externalId: input.asin,
marketplace: "www.amazon.com",
title: input.title,
brand: input.brand,
images: input.images || [],
/** Catalog is source of truth — never merge polluted library galleries */
imagesMode: input.imagesMode || "replace",
price: input.price ?? null,
rating: input.rating ?? null,
reviewCount: input.reviewCount ?? null,
fetchStatus: input.fetchStatus,
attributes: input.attributes || {},
},
});
}

export async function linkSiteItem(input) {
return req("/api/library/site-items", {
method: "POST",
auth: true,
json: {
siteId: input.siteId,
networkId: "amazon",
externalId: input.asin,
marketplace: "www.amazon.com",
slug: input.slug,
houseName: input.houseName,
category: input.category,
tagline: input.tagline,
status: input.status || "active",
limitedTime: !!input.limitedTime,
period: input.period || null,
},
});
}

export async function health() {
return req("/api/health");
}
Loading
Loading