- CatalogSearchOverlay: replace handleAddStub with real openAddToCollection/openAddToThread routing based on catalogSearchMode - ConfirmDialog + __root.tsx: swap t() for Trans component on deleteItemMessage, deleteCandidateMessage, pickWinnerMessage — fixes <bold> rendering as literal text - Biome format pass: fix 23 lint/format errors across scripts, services, tests - Planning: mark all UAT and verification gaps resolved for phases 07, 11, 16, 20, 21, 22, 24, 32, 34; close debug sessions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
139 lines
3.8 KiB
TypeScript
139 lines
3.8 KiB
TypeScript
import { z } from "zod";
|
|
import type { db as prodDb } from "../../../db/index.ts";
|
|
import {
|
|
bulkUpsertGlobalItems,
|
|
upsertGlobalItem,
|
|
} from "../../services/global-item.service.ts";
|
|
|
|
type Db = typeof prodDb;
|
|
|
|
interface ToolResult {
|
|
content: Array<{ type: "text"; text: string }>;
|
|
}
|
|
|
|
function textResult(data: unknown): ToolResult {
|
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
}
|
|
|
|
function errorResult(message: string): ToolResult {
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
};
|
|
}
|
|
|
|
const catalogItemInputSchema = {
|
|
manufacturerSlug: z
|
|
.string()
|
|
.describe("Manufacturer slug (e.g. 'revelate-designs', 'apidura')"),
|
|
model: z
|
|
.string()
|
|
.describe(
|
|
"Model name — combined with manufacturerSlug forms the unique identifier",
|
|
),
|
|
category: z
|
|
.string()
|
|
.optional()
|
|
.describe("Category name (e.g., 'Bags', 'Lights')"),
|
|
weightGrams: z.number().optional().describe("Weight in grams"),
|
|
priceCents: z
|
|
.number()
|
|
.optional()
|
|
.describe("MSRP price in cents (e.g., 9999 = $99.99)"),
|
|
imageUrl: z.string().optional().describe("URL to the product image"),
|
|
description: z.string().optional().describe("Product description"),
|
|
sourceUrl: z
|
|
.string()
|
|
.optional()
|
|
.describe("URL to the product page on manufacturer/retailer site"),
|
|
imageCredit: z
|
|
.string()
|
|
.optional()
|
|
.describe("Image credit — photographer or source name"),
|
|
imageSourceUrl: z
|
|
.string()
|
|
.optional()
|
|
.describe("Original URL where the image was sourced from"),
|
|
tags: z
|
|
.array(z.string())
|
|
.optional()
|
|
.describe("Tags for categorization (created automatically if new)"),
|
|
};
|
|
|
|
export const catalogToolDefinitions = [
|
|
{
|
|
name: "upsert_catalog_item",
|
|
description:
|
|
"Add or update a single item in the global catalog. If an item with the same brand and model already exists, it will be updated. Includes attribution fields for image credit and source tracking. Requires authentication.",
|
|
inputSchema: catalogItemInputSchema,
|
|
},
|
|
{
|
|
name: "bulk_upsert_catalog",
|
|
description:
|
|
"Add or update multiple items in the global catalog in a single batch (max 100). All items are processed in one transaction — if any item fails, the entire batch is rolled back. Each item is upserted on (brand, model) uniqueness.",
|
|
inputSchema: {
|
|
items: z
|
|
.array(z.object(catalogItemInputSchema))
|
|
.max(100)
|
|
.describe("Array of catalog items to upsert (max 100 per batch)"),
|
|
},
|
|
},
|
|
];
|
|
|
|
// Catalog tools operate on shared catalog — no userId needed for data scoping
|
|
// db is passed for database access
|
|
export function registerCatalogTools(db: Db) {
|
|
return {
|
|
upsert_catalog_item: async (args: {
|
|
manufacturerSlug: string;
|
|
model: string;
|
|
category?: string;
|
|
weightGrams?: number;
|
|
priceCents?: number;
|
|
imageUrl?: string;
|
|
description?: string;
|
|
sourceUrl?: string;
|
|
imageCredit?: string;
|
|
imageSourceUrl?: string;
|
|
tags?: string[];
|
|
}): Promise<ToolResult> => {
|
|
try {
|
|
const result = await upsertGlobalItem(db, args);
|
|
return textResult({
|
|
...result.item,
|
|
created: result.created,
|
|
});
|
|
} catch (err) {
|
|
return errorResult((err as Error).message);
|
|
}
|
|
},
|
|
|
|
bulk_upsert_catalog: async (args: {
|
|
items: Array<{
|
|
manufacturerSlug: string;
|
|
model: string;
|
|
category?: string;
|
|
weightGrams?: number;
|
|
priceCents?: number;
|
|
imageUrl?: string;
|
|
description?: string;
|
|
sourceUrl?: string;
|
|
imageCredit?: string;
|
|
imageSourceUrl?: string;
|
|
tags?: string[];
|
|
}>;
|
|
}): Promise<ToolResult> => {
|
|
try {
|
|
const result = await bulkUpsertGlobalItems(db, args.items);
|
|
return textResult({
|
|
created: result.created,
|
|
updated: result.updated,
|
|
totalProcessed: result.items.length,
|
|
items: result.items,
|
|
});
|
|
} catch (err) {
|
|
return errorResult((err as Error).message);
|
|
}
|
|
},
|
|
};
|
|
}
|