@opencommerceprotocol/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,3007 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+ var import_chalk12 = __toESM(require("chalk"));
29
+
30
+ // src/commands/init.ts
31
+ var import_fs = require("fs");
32
+ var import_path = __toESM(require("path"));
33
+ var import_chalk = __toESM(require("chalk"));
34
+ var import_spec = require("@opencommerceprotocol/spec");
35
+ async function runInit(outputDir) {
36
+ const { default: inquirer } = await import("inquirer");
37
+ console.log(import_chalk.default.bold.cyan("\n\u{1F310} Open Commerce Protocol \u2014 Init Wizard\n"));
38
+ console.log("This wizard will generate your OCP manifest, protocol description, and a starter product feed.\n");
39
+ const answers = await inquirer.prompt([
40
+ {
41
+ type: "input",
42
+ name: "name",
43
+ message: "Store name:",
44
+ validate: (v) => v.trim().length > 0 || "Store name is required"
45
+ },
46
+ {
47
+ type: "input",
48
+ name: "url",
49
+ message: "Store URL:",
50
+ default: "https://mystore.com",
51
+ validate: (v) => {
52
+ try {
53
+ new URL(v);
54
+ return true;
55
+ } catch {
56
+ return "Enter a valid URL (e.g. https://mystore.com)";
57
+ }
58
+ }
59
+ },
60
+ {
61
+ type: "list",
62
+ name: "platform",
63
+ message: "Platform:",
64
+ choices: [
65
+ { name: "WooCommerce (WordPress)", value: "woocommerce" },
66
+ { name: "Shopify", value: "shopify" },
67
+ { name: "Magento / Adobe Commerce", value: "magento" },
68
+ { name: "Custom / Other", value: "custom" }
69
+ ]
70
+ },
71
+ {
72
+ type: "input",
73
+ name: "currency",
74
+ message: "Primary currency (ISO 4217):",
75
+ default: "USD",
76
+ validate: (v) => /^[A-Z]{3}$/.test(v.trim()) || "Enter a 3-letter currency code (e.g. USD)",
77
+ filter: (v) => v.trim().toUpperCase()
78
+ },
79
+ {
80
+ type: "checkbox",
81
+ name: "capabilities",
82
+ message: "Capabilities (select all that apply):",
83
+ choices: [
84
+ { name: "Catalog \u2014 serve product listings", value: "catalog", checked: true },
85
+ { name: "Search \u2014 full-text search", value: "search", checked: true },
86
+ { name: "Cart \u2014 add/update cart items", value: "cart", checked: true },
87
+ { name: "Checkout \u2014 begin checkout flow", value: "checkout", checked: false },
88
+ { name: "Orders \u2014 track order status", value: "orders", checked: false }
89
+ ]
90
+ },
91
+ {
92
+ type: "confirm",
93
+ name: "includeRuntime",
94
+ message: "Include OCP runtime script in manifest?",
95
+ default: true
96
+ }
97
+ ]);
98
+ const storeUrl = answers.url.replace(/\/$/, "");
99
+ const feedUrl = `${storeUrl}/ocp/products.jsonl`;
100
+ const manifest = {
101
+ version: import_spec.OCP_VERSION,
102
+ merchant: {
103
+ name: answers.name,
104
+ url: storeUrl,
105
+ currency: answers.currency
106
+ },
107
+ capabilities: {
108
+ catalog: answers.capabilities.includes("catalog"),
109
+ search: answers.capabilities.includes("search"),
110
+ cart: answers.capabilities.includes("cart"),
111
+ checkout: answers.capabilities.includes("checkout") ? "redirect" : "none",
112
+ orders: answers.capabilities.includes("orders")
113
+ },
114
+ discovery: {
115
+ feed: feedUrl,
116
+ feed_format: "jsonl",
117
+ total_products: 0
118
+ },
119
+ permissions: {
120
+ requires_human_checkout: true
121
+ }
122
+ };
123
+ if (answers.includeRuntime) {
124
+ manifest.interact = {
125
+ runtime: "https://cdn.opencommerceprotocol.org/runtime/v1/ocp-runtime.min.js",
126
+ webmcp: true,
127
+ tools: [
128
+ ...answers.capabilities.includes("catalog") ? ["search_products", "get_product"] : [],
129
+ ...answers.capabilities.includes("cart") ? ["add_to_cart", "get_cart", "update_cart"] : [],
130
+ ...answers.capabilities.includes("checkout") ? ["begin_checkout"] : []
131
+ ]
132
+ };
133
+ }
134
+ const wellKnownDir = import_path.default.join(outputDir, ".well-known");
135
+ const ocpDir = import_path.default.join(outputDir, "ocp");
136
+ await import_fs.promises.mkdir(wellKnownDir, { recursive: true });
137
+ await import_fs.promises.mkdir(ocpDir, { recursive: true });
138
+ const manifestPath = import_path.default.join(wellKnownDir, "ocp.json");
139
+ await import_fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
140
+ const ocpMdContent = generateOcpMd(answers, storeUrl);
141
+ await import_fs.promises.writeFile(import_path.default.join(outputDir, "ocp.md"), ocpMdContent, "utf-8");
142
+ const starterFeed = generateStarterFeed(answers.name, storeUrl, answers.currency);
143
+ await import_fs.promises.writeFile(import_path.default.join(ocpDir, "products.jsonl"), starterFeed, "utf-8");
144
+ console.log("\n" + import_chalk.default.bold.green("\u2713 Generated:"));
145
+ console.log(import_chalk.default.green(` .well-known/ocp.json`));
146
+ console.log(import_chalk.default.green(` ocp.md`));
147
+ console.log(import_chalk.default.green(` ocp/products.jsonl`) + import_chalk.default.dim(" (example template \u2014 replace with real products)"));
148
+ console.log("\n" + import_chalk.default.bold("Next steps:"));
149
+ console.log(" 1. Edit " + import_chalk.default.cyan("ocp/products.jsonl") + " with your real product data");
150
+ console.log(" 2. Serve " + import_chalk.default.cyan(".well-known/ocp.json") + " and " + import_chalk.default.cyan("ocp.md") + " from your web server");
151
+ console.log(" 3. Run " + import_chalk.default.cyan("npx @opencommerceprotocol/cli validate " + storeUrl) + " to verify your implementation");
152
+ console.log(" 4. (Optional) Add the OCP runtime script to your storefront pages\n");
153
+ }
154
+ function generateOcpMd(answers, storeUrl) {
155
+ return `# ${answers.name} \u2014 Open Commerce Protocol
156
+
157
+ ## About This Store
158
+
159
+ ${answers.name} is an online store located at ${storeUrl}.
160
+
161
+ ## What We Sell
162
+
163
+ Describe your product catalog here. Include:
164
+ - Main product categories
165
+ - Price ranges
166
+ - Target customers
167
+ - Any specializations or unique offerings
168
+
169
+ ## How to Shop
170
+
171
+ 1. Use \`search_products\` to find products by keyword or category
172
+ 2. Use \`get_product\` to get detailed information on a specific item
173
+ 3. Use \`add_to_cart\` to add items to the cart
174
+ 4. Use \`begin_checkout\` to proceed to checkout (a human must complete payment)
175
+
176
+ ## Policies
177
+
178
+ - **Returns**: Describe your return policy
179
+ - **Shipping**: Describe shipping options and timelines
180
+ - **Currency**: All prices in ${answers.currency}
181
+
182
+ ## Agent Notes
183
+
184
+ - All purchases require human confirmation at checkout
185
+ - Product availability is updated in real-time
186
+ - Contact us for bulk orders or special requests
187
+
188
+ *This document is optimized for AI agents. For the human-readable store, visit ${storeUrl}*
189
+ `;
190
+ }
191
+ function generateStarterFeed(storeName, storeUrl, currency) {
192
+ const examples = [
193
+ {
194
+ id: "example-001",
195
+ name: "Example Product 1",
196
+ description: "Replace this with your real product description.",
197
+ price: 29.99,
198
+ currency,
199
+ url: `${storeUrl}/products/example-001`,
200
+ in_stock: true,
201
+ category: "Example Category",
202
+ agent_notes: "Replace with helpful notes for AI agents about this product.",
203
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
204
+ },
205
+ {
206
+ id: "example-002",
207
+ name: "Example Product 2",
208
+ description: `Another example product from ${storeName}.`,
209
+ price: 49.99,
210
+ currency,
211
+ url: `${storeUrl}/products/example-002`,
212
+ in_stock: true,
213
+ category: "Example Category",
214
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
215
+ }
216
+ ];
217
+ return examples.map((p) => JSON.stringify(p)).join("\n") + "\n";
218
+ }
219
+
220
+ // src/commands/validate.ts
221
+ var import_chalk2 = __toESM(require("chalk"));
222
+ var import_validator = require("@opencommerceprotocol/validator");
223
+ function scoreColor(score) {
224
+ if (score >= 80) return import_chalk2.default.green(String(score));
225
+ if (score >= 50) return import_chalk2.default.yellow(String(score));
226
+ return import_chalk2.default.red(String(score));
227
+ }
228
+ function check(ok, msg) {
229
+ console.log((ok ? import_chalk2.default.green(" \u2713") : import_chalk2.default.red(" \u2717")) + " " + msg);
230
+ }
231
+ async function runValidate(target) {
232
+ const validator = new import_validator.OCPValidator();
233
+ if (target.startsWith("http://") || target.startsWith("https://")) {
234
+ await validateRemote(validator, target);
235
+ } else {
236
+ await validateLocal(validator, target);
237
+ }
238
+ }
239
+ async function validateRemote(validator, url) {
240
+ const { default: ora } = await import("ora");
241
+ const spinner = ora(`Validating ${import_chalk2.default.cyan(url)}`).start();
242
+ let result;
243
+ try {
244
+ result = await validator.validateRemote(url);
245
+ spinner.stop();
246
+ } catch (err) {
247
+ spinner.fail(`Failed to validate: ${err instanceof Error ? err.message : String(err)}`);
248
+ process.exit(1);
249
+ }
250
+ console.log("\n" + import_chalk2.default.bold("OCP Validation Report"));
251
+ console.log(import_chalk2.default.dim("\u2501".repeat(50)));
252
+ console.log(import_chalk2.default.bold("URL:"), result.url);
253
+ console.log();
254
+ console.log(import_chalk2.default.bold("Manifest (/.well-known/ocp.json)"));
255
+ check(result.manifest.found, result.manifest.found ? "Found" : "Not found");
256
+ if (result.manifest.found) {
257
+ check(result.manifest.valid, result.manifest.valid ? "Valid" : "Invalid");
258
+ if (!result.manifest.valid) {
259
+ for (const err of result.manifest.errors.slice(0, 5)) {
260
+ console.log(import_chalk2.default.red(` ${err.path}: ${err.message}`));
261
+ }
262
+ }
263
+ }
264
+ console.log();
265
+ console.log(import_chalk2.default.bold("Protocol Description (/ocp.md)"));
266
+ check(result.description.found, result.description.found ? "Found" : "Not found");
267
+ if (result.description.found) {
268
+ check(result.description.wellFormatted, result.description.wellFormatted ? "Well-formatted" : "Minimal content (expand for better agent understanding)");
269
+ }
270
+ console.log();
271
+ console.log(import_chalk2.default.bold("Product Feed"));
272
+ check(result.feed.found, result.feed.found ? "Found" : "Not found");
273
+ if (result.feed.found && result.feed.result) {
274
+ const { totalProducts, validProducts, invalidProducts } = result.feed.result;
275
+ console.log(` ${import_chalk2.default.dim("\u2500")} ${totalProducts} products: ${import_chalk2.default.green(String(validProducts))} valid, ${invalidProducts > 0 ? import_chalk2.default.red(String(invalidProducts)) : "0"} invalid`);
276
+ if (result.feed.result.agentNotesQuality) {
277
+ const q = result.feed.result.agentNotesQuality;
278
+ const pct = totalProducts > 0 ? Math.round(q.withNotes / totalProducts * 100) : 0;
279
+ const color = pct >= 80 ? import_chalk2.default.green : pct >= 50 ? import_chalk2.default.yellow : import_chalk2.default.red;
280
+ console.log(` ${import_chalk2.default.dim("\u2500")} agent_notes: ${color(`${pct}%`)} coverage, avg score ${q.avgScore}/100`);
281
+ if (q.missing > 0) {
282
+ console.log(` ${import_chalk2.default.yellow(`${q.missing} products missing agent_notes`)}`);
283
+ }
284
+ if (q.poor > 0) {
285
+ console.log(` ${import_chalk2.default.yellow(`${q.poor} products have poor quality notes (score < 50)`)}`);
286
+ }
287
+ }
288
+ }
289
+ console.log();
290
+ console.log(import_chalk2.default.bold("Bridges"));
291
+ check(result.bridges.mcp, result.bridges.mcp ? "MCP bridge" : "MCP bridge (not configured)");
292
+ check(result.bridges.ucp, result.bridges.ucp ? "UCP bridge" : "UCP bridge (not configured)");
293
+ check(result.bridges.acp, result.bridges.acp ? "ACP bridge" : "ACP bridge (not configured)");
294
+ check(result.bridges.a2a, result.bridges.a2a ? "A2A bridge" : "A2A bridge (not configured)");
295
+ console.log();
296
+ console.log(import_chalk2.default.bold("Discovery"));
297
+ try {
298
+ const robotsResult = await (0, import_validator.validateRobotsTxt)(result.url);
299
+ check(robotsResult.hasOcpDirective, robotsResult.hasOcpDirective ? "robots.txt OCP-Manifest directive" : "robots.txt OCP-Manifest directive (not found)");
300
+ } catch {
301
+ check(false, "robots.txt (could not check)");
302
+ }
303
+ try {
304
+ const llmsResult = await (0, import_validator.validateLlmsTxt)(result.url);
305
+ check(llmsResult.hasLlmsTxt, llmsResult.hasLlmsTxt ? "llms.txt found" : "llms.txt (not found)");
306
+ if (llmsResult.hasLlmsTxt) {
307
+ check(llmsResult.hasCommerceSection, llmsResult.hasCommerceSection ? "llms.txt ## Commerce section" : "llms.txt ## Commerce section (not found)");
308
+ }
309
+ } catch {
310
+ check(false, "llms.txt (could not check)");
311
+ }
312
+ if (result.recommendations.length > 0) {
313
+ console.log();
314
+ console.log(import_chalk2.default.bold("Recommendations"));
315
+ for (const rec of result.recommendations) {
316
+ console.log(import_chalk2.default.yellow(" \u2192 ") + rec);
317
+ }
318
+ }
319
+ console.log();
320
+ console.log(import_chalk2.default.dim("\u2501".repeat(50)));
321
+ console.log(import_chalk2.default.bold("Score: ") + scoreColor(result.score) + import_chalk2.default.dim("/100"));
322
+ if (result.score === 100) {
323
+ console.log(import_chalk2.default.green.bold("\n\u{1F389} Perfect score! Your store is fully OCP-compliant.\n"));
324
+ } else if (result.score >= 80) {
325
+ console.log(import_chalk2.default.green("\n\u2713 Good implementation. Follow the recommendations to improve.\n"));
326
+ } else if (result.score >= 50) {
327
+ console.log(import_chalk2.default.yellow("\n\u26A0 Partial implementation. Follow the recommendations to complete setup.\n"));
328
+ } else {
329
+ console.log(import_chalk2.default.red("\n\u2717 Incomplete implementation. See recommendations above.\n"));
330
+ }
331
+ }
332
+ async function validateLocal(validator, dirPath) {
333
+ const { promises: fs8 } = await import("fs");
334
+ const path7 = await import("path");
335
+ console.log("\n" + import_chalk2.default.bold("OCP Local Validation"));
336
+ console.log(import_chalk2.default.dim("\u2501".repeat(50)));
337
+ console.log(import_chalk2.default.bold("Directory:"), dirPath);
338
+ console.log();
339
+ const manifestPath = path7.join(dirPath, ".well-known", "ocp.json");
340
+ try {
341
+ const raw = await fs8.readFile(manifestPath, "utf-8");
342
+ const manifest = JSON.parse(raw);
343
+ const result = validator.validateManifest(manifest);
344
+ check(true, ".well-known/ocp.json \u2014 found");
345
+ check(result.valid, result.valid ? "Valid manifest" : "Invalid manifest");
346
+ if (!result.valid) {
347
+ for (const err of result.errors.slice(0, 10)) {
348
+ console.log(import_chalk2.default.red(` ${err.path}: ${err.message}`));
349
+ }
350
+ }
351
+ } catch {
352
+ check(false, ".well-known/ocp.json \u2014 not found");
353
+ }
354
+ try {
355
+ const md = await fs8.readFile(path7.join(dirPath, "ocp.md"), "utf-8");
356
+ check(true, `ocp.md \u2014 found (${md.length} bytes)`);
357
+ } catch {
358
+ check(false, "ocp.md \u2014 not found");
359
+ }
360
+ const feedPath = path7.join(dirPath, "ocp", "products.jsonl");
361
+ try {
362
+ const raw = await fs8.readFile(feedPath, "utf-8");
363
+ const lines = raw.split("\n").filter((l) => l.trim());
364
+ let valid = 0;
365
+ let invalid = 0;
366
+ for (const line of lines) {
367
+ try {
368
+ const product = JSON.parse(line);
369
+ const result = validator.validateProduct(product);
370
+ if (result.valid) valid++;
371
+ else invalid++;
372
+ } catch {
373
+ invalid++;
374
+ }
375
+ }
376
+ check(true, `ocp/products.jsonl \u2014 ${lines.length} products (${valid} valid, ${invalid} invalid)`);
377
+ } catch {
378
+ check(false, "ocp/products.jsonl \u2014 not found");
379
+ }
380
+ try {
381
+ const robotsContent = await fs8.readFile(path7.join(dirPath, "robots.txt"), "utf-8");
382
+ const hasOcp = robotsContent.toLowerCase().includes("ocp-manifest:");
383
+ check(hasOcp, hasOcp ? "robots.txt \u2014 OCP-Manifest directive present" : "robots.txt \u2014 found but no OCP-Manifest directive");
384
+ } catch {
385
+ check(false, "robots.txt \u2014 not found");
386
+ }
387
+ try {
388
+ const llmsContent = await fs8.readFile(path7.join(dirPath, "llms.txt"), "utf-8");
389
+ const hasCommerce = llmsContent.includes("## Commerce");
390
+ check(hasCommerce, hasCommerce ? "llms.txt \u2014 ## Commerce section present" : "llms.txt \u2014 found but no ## Commerce section");
391
+ } catch {
392
+ check(false, "llms.txt \u2014 not found");
393
+ }
394
+ console.log();
395
+ }
396
+
397
+ // src/commands/generate.ts
398
+ var import_fs2 = require("fs");
399
+ var import_path2 = __toESM(require("path"));
400
+ var import_chalk3 = __toESM(require("chalk"));
401
+ var import_spec2 = require("@opencommerceprotocol/spec");
402
+ function mapRow(row, currency) {
403
+ const id = row["id"] || row["sku"] || row["product_id"];
404
+ const name = row["name"] || row["title"] || row["product_name"];
405
+ const priceRaw = row["price"] || row["sale_price"] || row["regular_price"];
406
+ if (!id || !name || !priceRaw) return null;
407
+ const price = parseFloat(String(priceRaw).replace(/[^0-9.]/g, ""));
408
+ if (isNaN(price)) return null;
409
+ return {
410
+ id: String(id),
411
+ name: String(name),
412
+ description: row["description"] || row["desc"] || row["body_html"],
413
+ price,
414
+ currency: row["currency"] || currency,
415
+ url: row["url"] || row["link"] || row["permalink"] || "",
416
+ image: row["image"] || row["img"] || row["image_src"] || row["featured_image"],
417
+ in_stock: row["in_stock"] !== void 0 ? Boolean(row["in_stock"]) : row["stock_status"] === "instock" || row["available"] === "true",
418
+ category: row["category"] || row["product_type"] || row["type"],
419
+ sku: row["sku"] || void 0,
420
+ brand: row["brand"] || row["vendor"] || void 0,
421
+ agent_notes: row["agent_notes"] || void 0,
422
+ updated_at: row["updated_at"] || row["date_modified"] || (/* @__PURE__ */ new Date()).toISOString()
423
+ };
424
+ }
425
+ async function runGenerate(options) {
426
+ const outputDir = options.output || ".";
427
+ const currency = "USD";
428
+ if (!options.products) {
429
+ console.error(import_chalk3.default.red("Error: --products file is required"));
430
+ process.exit(1);
431
+ }
432
+ const inputPath = import_path2.default.resolve(options.products);
433
+ const ext = import_path2.default.extname(inputPath).toLowerCase();
434
+ const raw = await import_fs2.promises.readFile(inputPath, "utf-8");
435
+ let products = [];
436
+ const errors = [];
437
+ if (ext === ".csv") {
438
+ const Papa = require("papaparse");
439
+ const { data } = Papa.parse(raw, { header: true, skipEmptyLines: true });
440
+ for (let i = 0; i < data.length; i++) {
441
+ const mapped = mapRow(data[i], currency);
442
+ if (mapped) {
443
+ products.push(mapped);
444
+ } else {
445
+ errors.push(`Row ${i + 2}: could not map required fields (id, name, price)`);
446
+ }
447
+ }
448
+ } else if (ext === ".json") {
449
+ const data = JSON.parse(raw);
450
+ for (let i = 0; i < data.length; i++) {
451
+ const mapped = mapRow(data[i], currency);
452
+ if (mapped) products.push(mapped);
453
+ else errors.push(`Item ${i}: could not map required fields`);
454
+ }
455
+ } else if (ext === ".jsonl") {
456
+ const lines = raw.split("\n").filter((l) => l.trim());
457
+ for (let i = 0; i < lines.length; i++) {
458
+ try {
459
+ const row = JSON.parse(lines[i]);
460
+ const mapped = mapRow(row, currency);
461
+ if (mapped) products.push(mapped);
462
+ else errors.push(`Line ${i + 1}: could not map required fields`);
463
+ } catch {
464
+ errors.push(`Line ${i + 1}: invalid JSON`);
465
+ }
466
+ }
467
+ } else {
468
+ console.error(import_chalk3.default.red(`Unsupported format: ${ext}. Use .csv, .json, or .jsonl`));
469
+ process.exit(1);
470
+ }
471
+ const ocpDir = import_path2.default.join(outputDir, "ocp");
472
+ await import_fs2.promises.mkdir(ocpDir, { recursive: true });
473
+ await import_fs2.promises.mkdir(import_path2.default.join(outputDir, ".well-known"), { recursive: true });
474
+ const feedPath = import_path2.default.join(ocpDir, "products.jsonl");
475
+ await import_fs2.promises.writeFile(feedPath, products.map((p) => JSON.stringify(p)).join("\n") + "\n", "utf-8");
476
+ const manifestPath = import_path2.default.join(outputDir, ".well-known", "ocp.json");
477
+ let manifest;
478
+ try {
479
+ manifest = JSON.parse(await import_fs2.promises.readFile(manifestPath, "utf-8"));
480
+ } catch {
481
+ manifest = {
482
+ version: import_spec2.OCP_VERSION,
483
+ merchant: { name: "My Store", url: "https://mystore.com" },
484
+ capabilities: { catalog: true }
485
+ };
486
+ }
487
+ const discovery = manifest["discovery"] || {};
488
+ discovery["total_products"] = products.length;
489
+ discovery["feed_updated"] = (/* @__PURE__ */ new Date()).toISOString();
490
+ manifest["discovery"] = discovery;
491
+ await import_fs2.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
492
+ const emap = {};
493
+ void emap;
494
+ console.log(import_chalk3.default.bold.green("\n\u2713 OCP files generated:"));
495
+ console.log(import_chalk3.default.green(` ocp/products.jsonl`), import_chalk3.default.dim(`\u2014 ${products.length} products`));
496
+ console.log(import_chalk3.default.green(` .well-known/ocp.json`), import_chalk3.default.dim("\u2014 updated total_products"));
497
+ if (errors.length > 0) {
498
+ console.log(import_chalk3.default.yellow(`
499
+ \u26A0 ${errors.length} rows skipped:`));
500
+ for (const e of errors.slice(0, 5)) {
501
+ console.log(import_chalk3.default.yellow(` ${e}`));
502
+ }
503
+ if (errors.length > 5) {
504
+ console.log(import_chalk3.default.yellow(` ...and ${errors.length - 5} more`));
505
+ }
506
+ }
507
+ console.log();
508
+ }
509
+
510
+ // src/commands/bridge.ts
511
+ var import_fs3 = require("fs");
512
+ var import_path3 = __toESM(require("path"));
513
+ var import_chalk4 = __toESM(require("chalk"));
514
+ async function runBridge(options) {
515
+ const outputDir = options.output || ".";
516
+ const manifestPath = options.manifest || ".well-known/ocp.json";
517
+ let manifest;
518
+ try {
519
+ manifest = JSON.parse(await import_fs3.promises.readFile(manifestPath, "utf-8"));
520
+ } catch {
521
+ console.error(import_chalk4.default.red(`Could not read manifest at ${manifestPath}`));
522
+ console.error(import_chalk4.default.dim("Run `ocp init` first to generate your manifest."));
523
+ process.exit(1);
524
+ }
525
+ switch (options.protocol.toLowerCase()) {
526
+ case "mcp":
527
+ await generateMCPBridge(manifest, outputDir);
528
+ break;
529
+ case "ucp":
530
+ await generateUCPBridge(manifest, outputDir);
531
+ break;
532
+ case "a2a":
533
+ await generateA2ABridge(manifest, outputDir);
534
+ break;
535
+ default:
536
+ console.error(import_chalk4.default.red(`Unknown protocol: ${options.protocol}`));
537
+ console.error("Supported protocols: mcp, ucp, a2a");
538
+ process.exit(1);
539
+ }
540
+ }
541
+ async function generateMCPBridge(manifest, outputDir) {
542
+ const merchant = manifest["merchant"];
543
+ const mcpConfig = {
544
+ name: `${merchant.name} MCP Server`,
545
+ version: "1.0.0",
546
+ ocp_manifest: `${merchant.url}/.well-known/ocp.json`,
547
+ transport: "stdio",
548
+ description: `MCP server bridging ${merchant.name} OCP tools`
549
+ };
550
+ const serverCode = `#!/usr/bin/env node
551
+ /**
552
+ * MCP Server Bridge for ${merchant.name}
553
+ * Generated by @opencommerceprotocol/cli
554
+ */
555
+ import { createMCPServer } from '@opencommerceprotocol/bridge-mcp';
556
+ import manifest from './ocp-manifest.json' assert { type: 'json' };
557
+
558
+ const server = createMCPServer(manifest);
559
+ server.start();
560
+ `;
561
+ await import_fs3.promises.mkdir(outputDir, { recursive: true });
562
+ await import_fs3.promises.writeFile(import_path3.default.join(outputDir, "mcp-server.js"), serverCode, "utf-8");
563
+ await import_fs3.promises.writeFile(
564
+ import_path3.default.join(outputDir, "ocp-manifest.json"),
565
+ JSON.stringify(manifest, null, 2),
566
+ "utf-8"
567
+ );
568
+ await import_fs3.promises.writeFile(
569
+ import_path3.default.join(outputDir, "mcp-config.json"),
570
+ JSON.stringify(mcpConfig, null, 2),
571
+ "utf-8"
572
+ );
573
+ console.log(import_chalk4.default.bold.green("\n\u2713 MCP bridge generated:"));
574
+ console.log(import_chalk4.default.green(` ${outputDir}/mcp-server.js`));
575
+ console.log(import_chalk4.default.green(` ${outputDir}/mcp-config.json`));
576
+ console.log("\n" + import_chalk4.default.bold("Next steps:"));
577
+ console.log(` 1. cd ${outputDir}`);
578
+ console.log(" 2. npm install @opencommerceprotocol/bridge-mcp");
579
+ console.log(" 3. node mcp-server.js\n");
580
+ }
581
+ async function generateUCPBridge(manifest, outputDir) {
582
+ const merchant = manifest["merchant"];
583
+ const caps = manifest["capabilities"] || {};
584
+ const ucpManifest = {
585
+ "@context": "https://schema.googleapis.com/ucp/v1",
586
+ type: "UniversalCheckoutProvider",
587
+ name: merchant.name,
588
+ url: merchant.url,
589
+ capabilities: {
590
+ checkout: caps["checkout"] !== "none",
591
+ catalog: caps["catalog"] === true,
592
+ search: caps["search"] === true
593
+ },
594
+ currency: merchant.currency || "USD",
595
+ ocp_source: `${merchant.url}/.well-known/ocp.json`
596
+ };
597
+ await import_fs3.promises.mkdir(outputDir, { recursive: true });
598
+ await import_fs3.promises.writeFile(
599
+ import_path3.default.join(outputDir, "ucp-manifest.json"),
600
+ JSON.stringify(ucpManifest, null, 2),
601
+ "utf-8"
602
+ );
603
+ console.log(import_chalk4.default.bold.green("\n\u2713 UCP bridge manifest generated:"));
604
+ console.log(import_chalk4.default.green(` ${outputDir}/ucp-manifest.json`));
605
+ console.log("\n" + import_chalk4.default.bold("Next steps:"));
606
+ console.log(" Serve this file at /.well-known/ucp on your domain\n");
607
+ }
608
+ async function generateA2ABridge(manifest, outputDir) {
609
+ const merchant = manifest["merchant"];
610
+ const interact = manifest["interact"];
611
+ const a2aCard = {
612
+ "@context": "https://schema.googleapis.com/a2a/v1",
613
+ type: "AgentCard",
614
+ name: `${merchant.name} Shopping Agent`,
615
+ description: merchant.description || `AI shopping agent for ${merchant.name}`,
616
+ url: merchant.url,
617
+ skills: (interact?.tools || ["search_products", "get_product"]).map((tool) => ({
618
+ id: tool,
619
+ name: tool.replace(/_/g, " "),
620
+ description: `OCP tool: ${tool}`
621
+ })),
622
+ ocp_source: `${merchant.url}/.well-known/ocp.json`
623
+ };
624
+ await import_fs3.promises.mkdir(outputDir, { recursive: true });
625
+ await import_fs3.promises.writeFile(
626
+ import_path3.default.join(outputDir, "a2a-agent-card.json"),
627
+ JSON.stringify(a2aCard, null, 2),
628
+ "utf-8"
629
+ );
630
+ console.log(import_chalk4.default.bold.green("\n\u2713 A2A agent card generated:"));
631
+ console.log(import_chalk4.default.green(` ${outputDir}/a2a-agent-card.json`));
632
+ console.log("\n" + import_chalk4.default.bold("Next steps:"));
633
+ console.log(" Serve this file at /.well-known/agent.json on your domain\n");
634
+ }
635
+
636
+ // src/commands/crawl.ts
637
+ var import_fs4 = require("fs");
638
+ var import_path4 = __toESM(require("path"));
639
+ var import_chalk5 = __toESM(require("chalk"));
640
+ var import_spec3 = require("@opencommerceprotocol/spec");
641
+
642
+ // src/utils/http.ts
643
+ var DEFAULT_USER_AGENT = "OCPBot/1.0 (+https://opencommerceprotocol.org/bot)";
644
+ var HttpClient = class {
645
+ userAgent;
646
+ minDelay;
647
+ retries;
648
+ timeout;
649
+ lastRequestTime = /* @__PURE__ */ new Map();
650
+ constructor(options = {}) {
651
+ this.userAgent = options.userAgent ?? DEFAULT_USER_AGENT;
652
+ this.minDelay = options.minDelay ?? 200;
653
+ this.retries = options.retries ?? 2;
654
+ this.timeout = options.timeout ?? 3e4;
655
+ }
656
+ /**
657
+ * Set the minimum delay between requests (e.g. from Crawl-delay).
658
+ * @param ms - Delay in milliseconds
659
+ */
660
+ setMinDelay(ms) {
661
+ this.minDelay = Math.max(100, ms);
662
+ }
663
+ /**
664
+ * Fetch a URL with rate limiting and retries.
665
+ * @param url - The URL to fetch
666
+ * @returns The HTTP response
667
+ */
668
+ async get(url) {
669
+ const host = new URL(url).host;
670
+ const lastTime = this.lastRequestTime.get(host) ?? 0;
671
+ const elapsed = Date.now() - lastTime;
672
+ if (elapsed < this.minDelay) {
673
+ await sleep(this.minDelay - elapsed);
674
+ }
675
+ let lastError;
676
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
677
+ try {
678
+ this.lastRequestTime.set(host, Date.now());
679
+ const controller = new AbortController();
680
+ const timer = setTimeout(() => controller.abort(), this.timeout);
681
+ const response = await fetch(url, {
682
+ headers: {
683
+ "User-Agent": this.userAgent,
684
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
685
+ },
686
+ signal: controller.signal,
687
+ redirect: "follow"
688
+ });
689
+ clearTimeout(timer);
690
+ const text = await response.text();
691
+ const headers = {};
692
+ response.headers.forEach((value, key) => {
693
+ headers[key] = value;
694
+ });
695
+ return {
696
+ status: response.status,
697
+ text,
698
+ headers,
699
+ url: response.url,
700
+ ok: response.ok
701
+ };
702
+ } catch (err) {
703
+ lastError = err instanceof Error ? err : new Error(String(err));
704
+ if (attempt < this.retries) {
705
+ await sleep(1e3 * (attempt + 1));
706
+ }
707
+ }
708
+ }
709
+ throw lastError ?? new Error(`Failed to fetch ${url}`);
710
+ }
711
+ };
712
+ function sleep(ms) {
713
+ return new Promise((resolve) => setTimeout(resolve, ms));
714
+ }
715
+
716
+ // src/utils/robots.ts
717
+ function parseRobotsTxt(content) {
718
+ const lines = content.split("\n");
719
+ const groups = [];
720
+ const sitemaps = [];
721
+ let ocpManifest;
722
+ let currentGroup = null;
723
+ for (const rawLine of lines) {
724
+ const line = rawLine.trim();
725
+ if (!line || line.startsWith("#")) {
726
+ continue;
727
+ }
728
+ const colonIdx = line.indexOf(":");
729
+ if (colonIdx === -1) continue;
730
+ const directive = line.substring(0, colonIdx).trim().toLowerCase();
731
+ const value = line.substring(colonIdx + 1).trim();
732
+ switch (directive) {
733
+ case "user-agent":
734
+ if (!currentGroup || currentGroup.rules.length > 0) {
735
+ currentGroup = { userAgents: [], rules: [] };
736
+ groups.push(currentGroup);
737
+ }
738
+ currentGroup.userAgents.push(value);
739
+ break;
740
+ case "allow":
741
+ if (currentGroup) {
742
+ currentGroup.rules.push({ type: "allow", path: value });
743
+ }
744
+ break;
745
+ case "disallow":
746
+ if (currentGroup) {
747
+ currentGroup.rules.push({ type: "disallow", path: value });
748
+ }
749
+ break;
750
+ case "crawl-delay":
751
+ if (currentGroup) {
752
+ const delay = parseFloat(value);
753
+ if (!isNaN(delay)) {
754
+ currentGroup.crawlDelay = delay;
755
+ }
756
+ }
757
+ break;
758
+ case "sitemap":
759
+ sitemaps.push(value);
760
+ break;
761
+ case "ocp-manifest":
762
+ ocpManifest = value;
763
+ break;
764
+ }
765
+ }
766
+ return { groups, sitemaps, ocpManifest, raw: content };
767
+ }
768
+ function isPathAllowed(robots, urlPath, userAgent = "*") {
769
+ const matchedGroup = findMatchingGroup(robots, userAgent);
770
+ if (!matchedGroup) return true;
771
+ let bestMatch;
772
+ let bestLength = -1;
773
+ for (const rule of matchedGroup.rules) {
774
+ if (urlPath.startsWith(rule.path) && rule.path.length > bestLength) {
775
+ bestMatch = rule;
776
+ bestLength = rule.path.length;
777
+ }
778
+ }
779
+ if (!bestMatch) return true;
780
+ return bestMatch.type === "allow";
781
+ }
782
+ function getCrawlDelay(robots, userAgent = "*") {
783
+ const matchedGroup = findMatchingGroup(robots, userAgent);
784
+ return matchedGroup?.crawlDelay;
785
+ }
786
+ function findMatchingGroup(robots, userAgent) {
787
+ const ua = userAgent.toLowerCase();
788
+ let wildcardGroup;
789
+ for (const group of robots.groups) {
790
+ for (const agent of group.userAgents) {
791
+ if (agent === "*") {
792
+ if (!wildcardGroup) wildcardGroup = group;
793
+ } else if (ua.includes(agent.toLowerCase())) {
794
+ return group;
795
+ }
796
+ }
797
+ }
798
+ return wildcardGroup;
799
+ }
800
+ function patchRobotsTxt(content, manifestUrl) {
801
+ const parsed = parseRobotsTxt(content);
802
+ if (parsed.ocpManifest) {
803
+ return content;
804
+ }
805
+ const directive = `OCP-Manifest: ${manifestUrl}`;
806
+ const lines = content.split("\n");
807
+ let lastSitemapIdx = -1;
808
+ for (let i = 0; i < lines.length; i++) {
809
+ if (lines[i].trim().toLowerCase().startsWith("sitemap:")) {
810
+ lastSitemapIdx = i;
811
+ }
812
+ }
813
+ if (lastSitemapIdx >= 0) {
814
+ lines.splice(lastSitemapIdx + 1, 0, directive);
815
+ } else {
816
+ const trimmed = content.trimEnd();
817
+ if (trimmed.length > 0) {
818
+ return trimmed + "\n" + directive + "\n";
819
+ }
820
+ return directive + "\n";
821
+ }
822
+ return lines.join("\n");
823
+ }
824
+
825
+ // src/utils/sitemap.ts
826
+ var DEFAULT_PRODUCT_PATTERNS = [
827
+ /\/products?\//i,
828
+ /\/shop\//i,
829
+ /\/item\//i,
830
+ /\/p\//i,
831
+ /\/catalog\//i,
832
+ /\/goods?\//i
833
+ ];
834
+ function parseSitemapXml(xml) {
835
+ const urls = [];
836
+ const childSitemaps = [];
837
+ const isIndex = /<sitemapindex[\s>]/i.test(xml);
838
+ if (isIndex) {
839
+ const sitemapRegex = /<sitemap[^>]*>([\s\S]*?)<\/sitemap>/gi;
840
+ let match;
841
+ while ((match = sitemapRegex.exec(xml)) !== null) {
842
+ const loc = extractTag(match[1], "loc");
843
+ if (loc) childSitemaps.push(loc);
844
+ }
845
+ } else {
846
+ const urlRegex = /<url[^>]*>([\s\S]*?)<\/url>/gi;
847
+ let match;
848
+ while ((match = urlRegex.exec(xml)) !== null) {
849
+ const loc = extractTag(match[1], "loc");
850
+ if (!loc) continue;
851
+ const entry = { loc };
852
+ const lastmod = extractTag(match[1], "lastmod");
853
+ if (lastmod) entry.lastmod = lastmod;
854
+ const changefreq = extractTag(match[1], "changefreq");
855
+ if (changefreq) entry.changefreq = changefreq;
856
+ const priority = extractTag(match[1], "priority");
857
+ if (priority) {
858
+ const p = parseFloat(priority);
859
+ if (!isNaN(p)) entry.priority = p;
860
+ }
861
+ urls.push(entry);
862
+ }
863
+ }
864
+ return { urls, isIndex, childSitemaps };
865
+ }
866
+ function extractTag(xml, tag) {
867
+ const regex = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i");
868
+ const match = regex.exec(xml);
869
+ return match?.[1]?.trim();
870
+ }
871
+ function filterProductUrls(urls, customPattern) {
872
+ const patterns = customPattern ? [customPattern] : DEFAULT_PRODUCT_PATTERNS;
873
+ return urls.filter((u) => {
874
+ try {
875
+ const path7 = new URL(u.loc).pathname;
876
+ return patterns.some((p) => p.test(path7));
877
+ } catch {
878
+ return false;
879
+ }
880
+ });
881
+ }
882
+ async function fetchSitemap(url, client, maxDepth = 3) {
883
+ if (maxDepth <= 0) return [];
884
+ const response = await client.get(url);
885
+ if (!response.ok) return [];
886
+ const result = parseSitemapXml(response.text);
887
+ if (result.isIndex) {
888
+ const allUrls = [];
889
+ for (const childUrl of result.childSitemaps) {
890
+ const childUrls = await fetchSitemap(childUrl, client, maxDepth - 1);
891
+ allUrls.push(...childUrls);
892
+ }
893
+ return allUrls;
894
+ }
895
+ return result.urls;
896
+ }
897
+
898
+ // src/utils/jsonld-extractor.ts
899
+ function extractJsonLdBlocks(html) {
900
+ const blocks = [];
901
+ const regex = /<script[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
902
+ let match;
903
+ while ((match = regex.exec(html)) !== null) {
904
+ try {
905
+ const parsed = JSON.parse(match[1]);
906
+ blocks.push(parsed);
907
+ } catch {
908
+ }
909
+ }
910
+ return blocks;
911
+ }
912
+ function findProductObjects(blocks) {
913
+ const products = [];
914
+ for (const block of blocks) {
915
+ if (!block || typeof block !== "object") continue;
916
+ const obj = block;
917
+ if (Array.isArray(obj["@graph"])) {
918
+ for (const item of obj["@graph"]) {
919
+ if (isProductType(item)) {
920
+ products.push(item);
921
+ }
922
+ }
923
+ continue;
924
+ }
925
+ if (Array.isArray(block)) {
926
+ for (const item of block) {
927
+ if (isProductType(item)) {
928
+ products.push(item);
929
+ }
930
+ }
931
+ continue;
932
+ }
933
+ if (isProductType(obj)) {
934
+ products.push(obj);
935
+ }
936
+ }
937
+ return products;
938
+ }
939
+ function isProductType(obj) {
940
+ if (!obj || typeof obj !== "object") return false;
941
+ const typed = obj;
942
+ const type = typed["@type"];
943
+ if (typeof type === "string") {
944
+ return type === "Product" || type === "https://schema.org/Product" || type === "schema:Product";
945
+ }
946
+ if (Array.isArray(type)) {
947
+ return type.some(
948
+ (t) => t === "Product" || t === "https://schema.org/Product" || t === "schema:Product"
949
+ );
950
+ }
951
+ return false;
952
+ }
953
+ function extractProductsFromHtml(html, sourceUrl) {
954
+ const blocks = extractJsonLdBlocks(html);
955
+ const productObjects = findProductObjects(blocks);
956
+ return productObjects.map((raw) => ({
957
+ raw,
958
+ sourceUrl
959
+ }));
960
+ }
961
+ function extractPageMetadata(html) {
962
+ const titleMatch = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
963
+ const descMatch = /<meta[^>]*name\s*=\s*["']description["'][^>]*content\s*=\s*["']([\s\S]*?)["'][^>]*>/i.exec(html) || /<meta[^>]*content\s*=\s*["']([\s\S]*?)["'][^>]*name\s*=\s*["']description["'][^>]*>/i.exec(html);
964
+ return {
965
+ title: titleMatch?.[1]?.trim(),
966
+ description: descMatch?.[1]?.trim()
967
+ };
968
+ }
969
+
970
+ // src/utils/schema-mapper.ts
971
+ var AVAILABILITY_MAP = {
972
+ "https://schema.org/InStock": true,
973
+ "http://schema.org/InStock": true,
974
+ "InStock": true,
975
+ "https://schema.org/LimitedAvailability": true,
976
+ "http://schema.org/LimitedAvailability": true,
977
+ "LimitedAvailability": true,
978
+ "https://schema.org/PreOrder": true,
979
+ "http://schema.org/PreOrder": true,
980
+ "PreOrder": true,
981
+ "https://schema.org/OnlineOnly": true,
982
+ "http://schema.org/OnlineOnly": true,
983
+ "OnlineOnly": true,
984
+ "https://schema.org/OutOfStock": false,
985
+ "http://schema.org/OutOfStock": false,
986
+ "OutOfStock": false,
987
+ "https://schema.org/Discontinued": false,
988
+ "http://schema.org/Discontinued": false,
989
+ "Discontinued": false,
990
+ "https://schema.org/SoldOut": false,
991
+ "http://schema.org/SoldOut": false,
992
+ "SoldOut": false
993
+ };
994
+ function mapSchemaOrgToOCP(product) {
995
+ const raw = product.raw;
996
+ const sourceUrl = product.sourceUrl;
997
+ const name = getString(raw, "name");
998
+ if (!name) return null;
999
+ const id = getString(raw, "sku") || generateIdFromUrl(sourceUrl);
1000
+ const offers = getOffers(raw);
1001
+ const primaryOffer = offers[0];
1002
+ const price = primaryOffer ? parsePrice(primaryOffer["price"]) : void 0;
1003
+ const currency = primaryOffer ? getString(primaryOffer, "priceCurrency") : void 0;
1004
+ if (price === void 0 || !currency) return null;
1005
+ const result = {
1006
+ id,
1007
+ name,
1008
+ price,
1009
+ currency,
1010
+ url: getString(raw, "url") || (primaryOffer ? getString(primaryOffer, "url") : void 0) || sourceUrl
1011
+ };
1012
+ const description = getString(raw, "description");
1013
+ if (description) result.description = description;
1014
+ const image = resolveImage(raw["image"], sourceUrl);
1015
+ if (image) result.image = image;
1016
+ const images = resolveImages(raw["image"], sourceUrl);
1017
+ if (images && images.length > 1) result.images = images;
1018
+ if (primaryOffer?.["availability"]) {
1019
+ const avail = String(primaryOffer["availability"]);
1020
+ if (avail in AVAILABILITY_MAP) {
1021
+ result.in_stock = AVAILABILITY_MAP[avail];
1022
+ }
1023
+ }
1024
+ const category = getString(raw, "category");
1025
+ if (category) result.category = category;
1026
+ const brand = extractBrandName(raw);
1027
+ if (brand) result.brand = brand;
1028
+ const sku = getString(raw, "sku");
1029
+ if (sku) result.sku = sku;
1030
+ const attributes = {};
1031
+ const gtin = getString(raw, "gtin") || getString(raw, "gtin13") || getString(raw, "gtin8") || getString(raw, "gtin12") || getString(raw, "gtin14");
1032
+ if (gtin) attributes["gtin"] = gtin;
1033
+ const aggregateRating = raw["aggregateRating"];
1034
+ if (aggregateRating && typeof aggregateRating === "object") {
1035
+ const ratingValue = parseFloat(String(aggregateRating["ratingValue"] ?? ""));
1036
+ if (!isNaN(ratingValue)) attributes["rating"] = ratingValue;
1037
+ const reviewCount = parseInt(String(aggregateRating["reviewCount"] ?? aggregateRating["ratingCount"] ?? ""), 10);
1038
+ if (!isNaN(reviewCount)) attributes["review_count"] = reviewCount;
1039
+ }
1040
+ const color = getString(raw, "color");
1041
+ if (color) attributes["color"] = color;
1042
+ const material = getString(raw, "material");
1043
+ if (material) attributes["material"] = material;
1044
+ const weight = raw["weight"];
1045
+ if (weight && typeof weight === "object") {
1046
+ const w = weight;
1047
+ const value = parseFloat(String(w["value"] ?? ""));
1048
+ const unit = getString(w, "unitText") || getString(w, "unitCode") || "g";
1049
+ if (!isNaN(value)) {
1050
+ attributes["weight"] = `${value} ${unit}`;
1051
+ result.weight = convertToGrams(value, unit);
1052
+ }
1053
+ }
1054
+ if (Object.keys(attributes).length > 0) {
1055
+ result.attributes = attributes;
1056
+ }
1057
+ const variants = extractVariants(raw, sourceUrl);
1058
+ if (variants.length > 0) result.variants = variants;
1059
+ const specs = extractSpecs(raw);
1060
+ if (specs.length > 0) result.specs = specs;
1061
+ if (primaryOffer) {
1062
+ const promotions = extractPromotions(primaryOffer);
1063
+ if (promotions.length > 0) result.promotions = promotions;
1064
+ const pricePerUnit = extractPricePerUnit(primaryOffer);
1065
+ if (pricePerUnit) result.price_per_unit = pricePerUnit;
1066
+ }
1067
+ result.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1068
+ return result;
1069
+ }
1070
+ function getString(obj, key) {
1071
+ const val = obj[key];
1072
+ if (typeof val === "string") return val.trim() || void 0;
1073
+ if (typeof val === "number") return String(val);
1074
+ return void 0;
1075
+ }
1076
+ function getOffers(raw) {
1077
+ const offers = raw["offers"];
1078
+ if (!offers) return [];
1079
+ if (Array.isArray(offers)) {
1080
+ return offers.filter((o) => typeof o === "object" && o !== null);
1081
+ }
1082
+ if (typeof offers === "object") {
1083
+ const o = offers;
1084
+ if (o["@type"] === "AggregateOffer") {
1085
+ const nestedOffers = o["offers"];
1086
+ if (Array.isArray(nestedOffers)) {
1087
+ return nestedOffers.filter((x) => typeof x === "object" && x !== null);
1088
+ }
1089
+ if (o["lowPrice"]) {
1090
+ return [{ ...o, price: o["lowPrice"] }];
1091
+ }
1092
+ }
1093
+ return [o];
1094
+ }
1095
+ return [];
1096
+ }
1097
+ function parsePrice(value) {
1098
+ if (typeof value === "number") return value;
1099
+ if (typeof value === "string") {
1100
+ const cleaned = value.replace(/[^0-9.,]/g, "").replace(/,(\d{3})/g, "$1").replace(/,/g, ".");
1101
+ const parsed = parseFloat(cleaned);
1102
+ return isNaN(parsed) ? void 0 : parsed;
1103
+ }
1104
+ return void 0;
1105
+ }
1106
+ function resolveImage(image, baseUrl) {
1107
+ if (typeof image === "string") return resolveUrl(image, baseUrl);
1108
+ if (Array.isArray(image)) {
1109
+ const first = image[0];
1110
+ if (typeof first === "string") return resolveUrl(first, baseUrl);
1111
+ if (typeof first === "object" && first !== null) {
1112
+ const url = first["url"] || first["contentUrl"];
1113
+ if (typeof url === "string") return resolveUrl(url, baseUrl);
1114
+ }
1115
+ }
1116
+ if (typeof image === "object" && image !== null) {
1117
+ const obj = image;
1118
+ const url = obj["url"] || obj["contentUrl"];
1119
+ if (typeof url === "string") return resolveUrl(url, baseUrl);
1120
+ }
1121
+ return void 0;
1122
+ }
1123
+ function resolveImages(image, baseUrl) {
1124
+ if (typeof image === "string") return [resolveUrl(image, baseUrl)];
1125
+ if (Array.isArray(image)) {
1126
+ const urls = [];
1127
+ for (const item of image) {
1128
+ if (typeof item === "string") {
1129
+ urls.push(resolveUrl(item, baseUrl));
1130
+ } else if (typeof item === "object" && item !== null) {
1131
+ const url = item["url"] || item["contentUrl"];
1132
+ if (typeof url === "string") urls.push(resolveUrl(url, baseUrl));
1133
+ }
1134
+ }
1135
+ return urls.length > 0 ? urls : void 0;
1136
+ }
1137
+ return void 0;
1138
+ }
1139
+ function resolveUrl(url, baseUrl) {
1140
+ try {
1141
+ return new URL(url, baseUrl).href;
1142
+ } catch {
1143
+ return url;
1144
+ }
1145
+ }
1146
+ function extractBrandName(raw) {
1147
+ const brand = raw["brand"];
1148
+ if (typeof brand === "string") return brand;
1149
+ if (typeof brand === "object" && brand !== null) {
1150
+ const b = brand;
1151
+ return getString(b, "name");
1152
+ }
1153
+ return void 0;
1154
+ }
1155
+ function generateIdFromUrl(url) {
1156
+ try {
1157
+ const pathname = new URL(url).pathname;
1158
+ const slug = pathname.split("/").filter(Boolean).pop();
1159
+ return slug || "unknown";
1160
+ } catch {
1161
+ return "unknown";
1162
+ }
1163
+ }
1164
+ function convertToGrams(value, unit) {
1165
+ const u = unit.toLowerCase();
1166
+ if (u === "kg" || u === "kgm") return value * 1e3;
1167
+ if (u === "lb" || u === "lbs" || u === "lbm") return value * 453.592;
1168
+ if (u === "oz" || u === "onz") return value * 28.3495;
1169
+ return value;
1170
+ }
1171
+ function extractSpecs(raw) {
1172
+ const specs = [];
1173
+ const additionalProperty = raw["additionalProperty"];
1174
+ if (!Array.isArray(additionalProperty)) return specs;
1175
+ for (const prop of additionalProperty) {
1176
+ if (typeof prop !== "object" || prop === null) continue;
1177
+ const p = prop;
1178
+ const name = getString(p, "name");
1179
+ if (!name) continue;
1180
+ let value;
1181
+ const rawValue = p["value"];
1182
+ if (typeof rawValue === "string") value = rawValue;
1183
+ else if (typeof rawValue === "number") value = rawValue;
1184
+ else if (typeof rawValue === "boolean") value = rawValue;
1185
+ else continue;
1186
+ const spec = { name, value };
1187
+ const unitText = getString(p, "unitText") || getString(p, "unitCode");
1188
+ if (unitText) spec.unit = unitText;
1189
+ const propertyID = getString(p, "propertyID");
1190
+ if (propertyID) spec.group = propertyID;
1191
+ specs.push(spec);
1192
+ }
1193
+ return specs;
1194
+ }
1195
+ function extractPromotions(offer) {
1196
+ const promotions = [];
1197
+ const priceSpec = offer["priceSpecification"];
1198
+ if (Array.isArray(priceSpec)) {
1199
+ for (const spec of priceSpec) {
1200
+ if (typeof spec !== "object" || spec === null) continue;
1201
+ const s = spec;
1202
+ const type = getString(s, "@type");
1203
+ if (type === "UnitPriceSpecification" || type === "CompoundPriceSpecification") continue;
1204
+ const description = getString(s, "name") || getString(s, "description");
1205
+ if (!description) continue;
1206
+ const discount2 = parsePrice(s["price"]);
1207
+ promotions.push({
1208
+ description,
1209
+ discount_type: "fixed",
1210
+ discount_value: discount2
1211
+ });
1212
+ }
1213
+ }
1214
+ const discount = offer["discount"];
1215
+ if (typeof discount === "number" || typeof discount === "string") {
1216
+ const val = typeof discount === "number" ? discount : parseFloat(discount);
1217
+ if (!isNaN(val) && val > 0) {
1218
+ promotions.push({
1219
+ description: "Discount",
1220
+ discount_type: "percentage",
1221
+ discount_value: val
1222
+ });
1223
+ }
1224
+ }
1225
+ return promotions;
1226
+ }
1227
+ function extractPricePerUnit(offer) {
1228
+ const priceSpec = offer["priceSpecification"];
1229
+ if (!priceSpec) return void 0;
1230
+ const specs = Array.isArray(priceSpec) ? priceSpec : [priceSpec];
1231
+ for (const spec of specs) {
1232
+ if (typeof spec !== "object" || spec === null) continue;
1233
+ const s = spec;
1234
+ if (getString(s, "@type") !== "UnitPriceSpecification") continue;
1235
+ const amount = parsePrice(s["price"]);
1236
+ const unit = getString(s, "unitText") || getString(s, "referenceQuantity") ? "unit" : void 0;
1237
+ if (amount !== void 0 && unit) {
1238
+ return { amount, unit: getString(s, "unitText") || "unit" };
1239
+ }
1240
+ }
1241
+ return void 0;
1242
+ }
1243
+ function extractVariants(raw, sourceUrl) {
1244
+ const variants = [];
1245
+ const hasVariant = raw["hasVariant"];
1246
+ if (Array.isArray(hasVariant)) {
1247
+ for (const v of hasVariant) {
1248
+ if (typeof v !== "object" || v === null) continue;
1249
+ const variant = v;
1250
+ const name = getString(variant, "name");
1251
+ const sku = getString(variant, "sku");
1252
+ const vOffers = getOffers(variant);
1253
+ const vPrice = vOffers[0] ? parsePrice(vOffers[0]["price"]) : void 0;
1254
+ if (name && vPrice !== void 0) {
1255
+ const vResult = {
1256
+ id: sku || generateIdFromUrl(sourceUrl) + "-" + variants.length,
1257
+ name,
1258
+ price: vPrice
1259
+ };
1260
+ if (vOffers[0]?.["availability"]) {
1261
+ const avail = String(vOffers[0]["availability"]);
1262
+ if (avail in AVAILABILITY_MAP) {
1263
+ vResult.in_stock = AVAILABILITY_MAP[avail];
1264
+ }
1265
+ }
1266
+ if (sku) vResult.sku = sku;
1267
+ const attrs = {};
1268
+ const color = getString(variant, "color");
1269
+ if (color) attrs["color"] = color;
1270
+ const size = getString(variant, "size");
1271
+ if (size) attrs["size"] = size;
1272
+ if (Object.keys(attrs).length > 0) vResult.attributes = attrs;
1273
+ variants.push(vResult);
1274
+ }
1275
+ }
1276
+ }
1277
+ if (variants.length === 0) {
1278
+ const offers = getOffers(raw);
1279
+ if (offers.length > 1) {
1280
+ for (let i = 0; i < offers.length; i++) {
1281
+ const offer = offers[i];
1282
+ const name = getString(offer, "name") || `Variant ${i + 1}`;
1283
+ const sku = getString(offer, "sku");
1284
+ const price = parsePrice(offer["price"]);
1285
+ if (price !== void 0) {
1286
+ const vResult = {
1287
+ id: sku || `variant-${i}`,
1288
+ name,
1289
+ price
1290
+ };
1291
+ if (offer["availability"]) {
1292
+ const avail = String(offer["availability"]);
1293
+ if (avail in AVAILABILITY_MAP) {
1294
+ vResult.in_stock = AVAILABILITY_MAP[avail];
1295
+ }
1296
+ }
1297
+ if (sku) vResult.sku = sku;
1298
+ variants.push(vResult);
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+ return variants;
1304
+ }
1305
+
1306
+ // src/utils/agent-notes.ts
1307
+ function generateAgentNotes(product) {
1308
+ const parts = [];
1309
+ const namePart = product.brand ? `${product.brand} ${product.name.toLowerCase()}` : product.name;
1310
+ parts.push(namePart + ".");
1311
+ if (product.category) {
1312
+ parts.push(`Category: ${product.category}.`);
1313
+ }
1314
+ const attrs = product.attributes ?? {};
1315
+ const rating = attrs["rating"];
1316
+ const reviewCount = attrs["review_count"];
1317
+ if (rating !== void 0) {
1318
+ let ratingStr = `${rating}/5`;
1319
+ if (reviewCount !== void 0) {
1320
+ ratingStr += ` from ${reviewCount} reviews`;
1321
+ }
1322
+ parts.push(ratingStr + ".");
1323
+ }
1324
+ const highlights = [];
1325
+ if (attrs["color"]) highlights.push(String(attrs["color"]));
1326
+ if (attrs["material"]) highlights.push(String(attrs["material"]));
1327
+ if (attrs["weight"]) highlights.push(String(attrs["weight"]));
1328
+ if (highlights.length > 0) {
1329
+ parts.push(highlights.join(", ") + ".");
1330
+ }
1331
+ if (product.variants && product.variants.length > 0) {
1332
+ parts.push(`Available in ${product.variants.length} variants.`);
1333
+ }
1334
+ if (product.original_price && product.original_price > product.price) {
1335
+ const discount = Math.round((1 - product.price / product.original_price) * 100);
1336
+ parts.push(`${discount}% off (was ${product.currency} ${product.original_price.toFixed(2)}).`);
1337
+ }
1338
+ if (product.in_stock === true) {
1339
+ parts.push("In stock.");
1340
+ } else if (product.in_stock === false) {
1341
+ parts.push("Out of stock.");
1342
+ }
1343
+ return parts.join(" ");
1344
+ }
1345
+
1346
+ // src/utils/llmstxt.ts
1347
+ function parseLlmsTxt(content) {
1348
+ const lines = content.split("\n");
1349
+ let title;
1350
+ let summary;
1351
+ const sections = [];
1352
+ const summaryLines = [];
1353
+ let currentSection = null;
1354
+ for (const line of lines) {
1355
+ if (line.startsWith("# ") && !title) {
1356
+ title = line.substring(2).trim();
1357
+ continue;
1358
+ }
1359
+ if (line.startsWith(">")) {
1360
+ summaryLines.push(line.substring(1).trim());
1361
+ continue;
1362
+ }
1363
+ if (line.startsWith("## ")) {
1364
+ currentSection = {
1365
+ heading: line.substring(3).trim(),
1366
+ links: [],
1367
+ rawLines: []
1368
+ };
1369
+ sections.push(currentSection);
1370
+ continue;
1371
+ }
1372
+ if (currentSection && line.trim().startsWith("- [")) {
1373
+ const linkMatch = /^-\s*\[([^\]]+)\]\(([^)]+)\)(?:\s*:\s*(.*))?/.exec(line.trim());
1374
+ if (linkMatch) {
1375
+ currentSection.links.push({
1376
+ text: linkMatch[1],
1377
+ url: linkMatch[2],
1378
+ description: linkMatch[3]?.trim()
1379
+ });
1380
+ }
1381
+ currentSection.rawLines.push(line);
1382
+ continue;
1383
+ }
1384
+ if (currentSection && line.trim()) {
1385
+ currentSection.rawLines.push(line);
1386
+ }
1387
+ }
1388
+ if (summaryLines.length > 0) {
1389
+ summary = summaryLines.join(" ");
1390
+ }
1391
+ return { title, summary, sections, raw: content };
1392
+ }
1393
+ function generateCommerceSection(data) {
1394
+ const lines = ["## Commerce"];
1395
+ if (data.manifestUrl) {
1396
+ lines.push(`- [Store Manifest](${data.manifestUrl}): Machine-readable commerce capabilities and product feed location`);
1397
+ }
1398
+ if (data.feedUrl) {
1399
+ let desc = "Full product catalog (JSONL";
1400
+ if (data.productCount !== void 0) {
1401
+ desc += `, ${data.productCount} products`;
1402
+ }
1403
+ if (data.updateFrequency) {
1404
+ desc += `, ${data.updateFrequency}`;
1405
+ }
1406
+ desc += ")";
1407
+ lines.push(`- [Product Catalog](${data.feedUrl}): ${desc}`);
1408
+ }
1409
+ if (data.ocpMdUrl) {
1410
+ lines.push(`- [Agent Storefront](${data.ocpMdUrl}): Store overview, policies, and categories for AI agent context`);
1411
+ }
1412
+ return lines.join("\n");
1413
+ }
1414
+ function patchLlmsTxt(content, data) {
1415
+ const parsed = parseLlmsTxt(content);
1416
+ const commerceSection = generateCommerceSection(data);
1417
+ const existingIdx = parsed.sections.findIndex(
1418
+ (s) => s.heading.toLowerCase() === "commerce"
1419
+ );
1420
+ const lines = content.split("\n");
1421
+ if (existingIdx >= 0) {
1422
+ const section = parsed.sections[existingIdx];
1423
+ const startLine = findSectionLine(lines, section.heading);
1424
+ if (startLine >= 0) {
1425
+ const endLine = findNextSectionLine(lines, startLine + 1);
1426
+ const newLines = commerceSection.split("\n");
1427
+ lines.splice(startLine, endLine - startLine, ...newLines);
1428
+ return lines.join("\n");
1429
+ }
1430
+ }
1431
+ const optionalIdx = parsed.sections.findIndex(
1432
+ (s) => s.heading.toLowerCase() === "optional"
1433
+ );
1434
+ if (optionalIdx >= 0) {
1435
+ const section = parsed.sections[optionalIdx];
1436
+ const startLine = findSectionLine(lines, section.heading);
1437
+ if (startLine >= 0) {
1438
+ lines.splice(startLine, 0, "", commerceSection, "");
1439
+ return lines.join("\n");
1440
+ }
1441
+ }
1442
+ const trimmed = content.trimEnd();
1443
+ return trimmed + "\n\n" + commerceSection + "\n";
1444
+ }
1445
+ function generateLlmsTxt(storeName, storeDescription, data) {
1446
+ const lines = [
1447
+ `# ${storeName}`,
1448
+ "",
1449
+ `> ${storeDescription}`,
1450
+ "",
1451
+ generateCommerceSection(data),
1452
+ ""
1453
+ ];
1454
+ return lines.join("\n");
1455
+ }
1456
+ function findSectionLine(lines, heading) {
1457
+ for (let i = 0; i < lines.length; i++) {
1458
+ if (lines[i].trim() === `## ${heading}`) {
1459
+ return i;
1460
+ }
1461
+ }
1462
+ return -1;
1463
+ }
1464
+ function findNextSectionLine(lines, startLine) {
1465
+ for (let i = startLine; i < lines.length; i++) {
1466
+ if (lines[i].startsWith("## ")) {
1467
+ return i;
1468
+ }
1469
+ }
1470
+ return lines.length;
1471
+ }
1472
+
1473
+ // src/commands/crawl.ts
1474
+ async function runCrawl(url, options) {
1475
+ const { default: ora } = await import("ora");
1476
+ const baseUrl = url.replace(/\/$/, "");
1477
+ console.log(import_chalk5.default.bold.cyan("\n\u{1F577} OCP Crawl \u2014 Schema.org Product Extraction\n"));
1478
+ console.log(import_chalk5.default.dim(`Target: ${baseUrl}`));
1479
+ console.log(import_chalk5.default.dim(`Output: ${options.output}`));
1480
+ console.log();
1481
+ const client = new HttpClient({
1482
+ userAgent: options.userAgent,
1483
+ minDelay: options.delay
1484
+ });
1485
+ const spinner = ora("Fetching robots.txt...").start();
1486
+ let robots;
1487
+ let existingManifestUrl;
1488
+ try {
1489
+ const robotsRes = await client.get(`${baseUrl}/robots.txt`);
1490
+ if (robotsRes.ok) {
1491
+ robots = parseRobotsTxt(robotsRes.text);
1492
+ existingManifestUrl = robots.ocpManifest;
1493
+ const crawlDelay = getCrawlDelay(robots, "OCPBot");
1494
+ if (crawlDelay) {
1495
+ const delayMs = Math.max(crawlDelay * 1e3, options.delay);
1496
+ client.setMinDelay(delayMs);
1497
+ spinner.text = `robots.txt found (Crawl-delay: ${crawlDelay}s)`;
1498
+ }
1499
+ spinner.succeed(`robots.txt found (${robots.sitemaps.length} sitemaps${existingManifestUrl ? ", OCP-Manifest detected" : ""})`);
1500
+ } else {
1501
+ spinner.info("No robots.txt found \u2014 proceeding with defaults");
1502
+ }
1503
+ } catch {
1504
+ spinner.info("Could not fetch robots.txt \u2014 proceeding with defaults");
1505
+ }
1506
+ const sitemapSpinner = ora("Fetching sitemap...").start();
1507
+ let allSitemapUrls = [];
1508
+ const sitemapUrls = robots?.sitemaps?.length ? robots.sitemaps : [`${baseUrl}/sitemap.xml`];
1509
+ for (const sitemapUrl of sitemapUrls) {
1510
+ try {
1511
+ const urls = await fetchSitemap(sitemapUrl, client);
1512
+ allSitemapUrls.push(...urls);
1513
+ } catch {
1514
+ if (options.verbose) {
1515
+ console.log(import_chalk5.default.dim(` Could not fetch sitemap: ${sitemapUrl}`));
1516
+ }
1517
+ }
1518
+ }
1519
+ if (allSitemapUrls.length === 0) {
1520
+ sitemapSpinner.warn("No sitemap found \u2014 cannot discover product URLs");
1521
+ console.log(import_chalk5.default.yellow(" Try providing a sitemap URL or use --product-pattern to specify URL patterns"));
1522
+ return;
1523
+ }
1524
+ sitemapSpinner.succeed(`Found ${allSitemapUrls.length} URLs in sitemap(s)`);
1525
+ const productPattern = options.productPattern ? new RegExp(options.productPattern, "i") : void 0;
1526
+ let productUrls = filterProductUrls(allSitemapUrls, productPattern);
1527
+ if (robots) {
1528
+ productUrls = productUrls.filter((u) => {
1529
+ try {
1530
+ const urlPath = new URL(u.loc).pathname;
1531
+ return isPathAllowed(robots, urlPath, "OCPBot");
1532
+ } catch {
1533
+ return true;
1534
+ }
1535
+ });
1536
+ }
1537
+ if (options.maxProducts) {
1538
+ productUrls = productUrls.slice(0, options.maxProducts);
1539
+ }
1540
+ console.log(import_chalk5.default.dim(`Identified ${productUrls.length} product URLs to crawl`));
1541
+ if (productUrls.length === 0) {
1542
+ console.log(import_chalk5.default.yellow("\nNo product URLs found. Try using --product-pattern to specify URL patterns."));
1543
+ return;
1544
+ }
1545
+ if (options.dryRun) {
1546
+ console.log(import_chalk5.default.bold("\nDry run \u2014 URLs that would be crawled:"));
1547
+ for (const u of productUrls) {
1548
+ console.log(` ${u.loc}`);
1549
+ }
1550
+ console.log(import_chalk5.default.dim(`
1551
+ Total: ${productUrls.length} URLs`));
1552
+ return;
1553
+ }
1554
+ let startIndex = 0;
1555
+ let products = [];
1556
+ const statePath = import_path4.default.join(options.output, ".ocp-crawl-state.json");
1557
+ if (options.resume) {
1558
+ try {
1559
+ const stateRaw = await import_fs4.promises.readFile(statePath, "utf-8");
1560
+ const state = JSON.parse(stateRaw);
1561
+ startIndex = state.lastCrawledIndex + 1;
1562
+ products = state.products;
1563
+ console.log(import_chalk5.default.dim(`Resuming from URL ${startIndex + 1}/${productUrls.length}`));
1564
+ } catch {
1565
+ }
1566
+ }
1567
+ let extracted = products.length;
1568
+ let failed = 0;
1569
+ let siteTitle;
1570
+ let siteDescription;
1571
+ const crawlSpinner = ora(`Crawling products... 0/${productUrls.length}`).start();
1572
+ const urlList = productUrls.map((u) => u.loc);
1573
+ for (let i = startIndex; i < urlList.length; i += options.concurrency) {
1574
+ const batch = urlList.slice(i, i + options.concurrency);
1575
+ const results = await Promise.allSettled(
1576
+ batch.map(async (productUrl) => {
1577
+ const response = await client.get(productUrl);
1578
+ if (!response.ok) return null;
1579
+ if (!siteTitle) {
1580
+ const meta = extractPageMetadata(response.text);
1581
+ if (meta.title) siteTitle = meta.title;
1582
+ if (meta.description) siteDescription = meta.description;
1583
+ }
1584
+ const schemaProducts = extractProductsFromHtml(response.text, productUrl);
1585
+ return schemaProducts;
1586
+ })
1587
+ );
1588
+ for (const result of results) {
1589
+ if (result.status === "fulfilled" && result.value) {
1590
+ for (const sp of result.value) {
1591
+ const ocpProduct = mapSchemaOrgToOCP(sp);
1592
+ if (ocpProduct) {
1593
+ if (options.generateAgentNotes && !ocpProduct.agent_notes) {
1594
+ ocpProduct.agent_notes = generateAgentNotes(ocpProduct);
1595
+ }
1596
+ products.push(ocpProduct);
1597
+ extracted++;
1598
+ } else {
1599
+ failed++;
1600
+ }
1601
+ }
1602
+ if (result.value.length === 0) failed++;
1603
+ } else {
1604
+ failed++;
1605
+ }
1606
+ }
1607
+ crawlSpinner.text = `Crawling products... ${Math.min(i + options.concurrency, urlList.length)}/${urlList.length} (${extracted} extracted)`;
1608
+ if (i % 50 === 0 && i > 0) {
1609
+ await import_fs4.promises.mkdir(options.output, { recursive: true });
1610
+ const state = {
1611
+ lastCrawledIndex: i + batch.length - 1,
1612
+ productUrls: urlList,
1613
+ products
1614
+ };
1615
+ await import_fs4.promises.writeFile(statePath, JSON.stringify(state), "utf-8");
1616
+ }
1617
+ }
1618
+ crawlSpinner.succeed(`Crawled ${urlList.length} URLs. Extracted ${extracted} products. ${failed} pages had no Product JSON-LD.`);
1619
+ const outputSpinner = ora("Generating output files...").start();
1620
+ const outputDir = options.output;
1621
+ const wellKnownDir = import_path4.default.join(outputDir, ".well-known");
1622
+ const ocpDir = import_path4.default.join(outputDir, "ocp");
1623
+ await import_fs4.promises.mkdir(wellKnownDir, { recursive: true });
1624
+ await import_fs4.promises.mkdir(ocpDir, { recursive: true });
1625
+ const feedPath = import_path4.default.join(ocpDir, "products.jsonl");
1626
+ await import_fs4.promises.writeFile(feedPath, products.map((p) => JSON.stringify(p)).join("\n") + "\n", "utf-8");
1627
+ const storeName = siteTitle?.replace(/ [-|–].*/g, "").trim() || new URL(baseUrl).hostname;
1628
+ const manifest = {
1629
+ version: import_spec3.OCP_VERSION,
1630
+ merchant: {
1631
+ name: storeName,
1632
+ url: baseUrl
1633
+ },
1634
+ capabilities: {
1635
+ catalog: true,
1636
+ search: true
1637
+ },
1638
+ discovery: {
1639
+ feed: `${baseUrl}/ocp/products.jsonl`,
1640
+ feed_format: "jsonl",
1641
+ feed_updated: (/* @__PURE__ */ new Date()).toISOString(),
1642
+ feed_generated_from: "schema_org_crawl",
1643
+ sitemap: sitemapUrls[0],
1644
+ total_products: products.length,
1645
+ schema_org: true,
1646
+ robots_txt: false,
1647
+ llms_txt: false
1648
+ },
1649
+ permissions: {
1650
+ requires_human_checkout: true
1651
+ }
1652
+ };
1653
+ const manifestPath = import_path4.default.join(wellKnownDir, "ocp.json");
1654
+ await import_fs4.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
1655
+ const categories = [...new Set(products.filter((p) => p.category).map((p) => p.category))];
1656
+ const priceRange = products.length > 0 ? { min: Math.min(...products.map((p) => p.price)), max: Math.max(...products.map((p) => p.price)) } : { min: 0, max: 0 };
1657
+ const ocpMdContent = generateOcpMd2(storeName, baseUrl, siteDescription, categories, products.length, priceRange, products[0]?.currency || "USD");
1658
+ await import_fs4.promises.writeFile(import_path4.default.join(outputDir, "ocp.md"), ocpMdContent, "utf-8");
1659
+ const robotsTxtPath = import_path4.default.join(outputDir, "robots.txt");
1660
+ const manifestUrl = `${baseUrl}/.well-known/ocp.json`;
1661
+ try {
1662
+ const existingRobots = await import_fs4.promises.readFile(robotsTxtPath, "utf-8");
1663
+ const patchedRobots = patchRobotsTxt(existingRobots, manifestUrl);
1664
+ await import_fs4.promises.writeFile(robotsTxtPath, patchedRobots, "utf-8");
1665
+ } catch {
1666
+ const newRobots = `User-agent: *
1667
+ Allow: /
1668
+
1669
+ Sitemap: ${baseUrl}/sitemap.xml
1670
+ OCP-Manifest: ${manifestUrl}
1671
+ `;
1672
+ await import_fs4.promises.writeFile(robotsTxtPath, newRobots, "utf-8");
1673
+ }
1674
+ manifest.discovery["robots_txt"] = true;
1675
+ const llmsTxtPath = import_path4.default.join(outputDir, "llms.txt");
1676
+ const commerceData = {
1677
+ manifestUrl: "/.well-known/ocp.json",
1678
+ feedUrl: "/ocp/products.jsonl",
1679
+ ocpMdUrl: "/ocp.md",
1680
+ productCount: products.length,
1681
+ updateFrequency: "updated daily"
1682
+ };
1683
+ try {
1684
+ const existingLlms = await import_fs4.promises.readFile(llmsTxtPath, "utf-8");
1685
+ const patchedLlms = patchLlmsTxt(existingLlms, commerceData);
1686
+ await import_fs4.promises.writeFile(llmsTxtPath, patchedLlms, "utf-8");
1687
+ } catch {
1688
+ const newLlms = generateLlmsTxt(storeName, siteDescription || `Online store at ${baseUrl}`, commerceData);
1689
+ await import_fs4.promises.writeFile(llmsTxtPath, newLlms, "utf-8");
1690
+ }
1691
+ manifest.discovery["llms_txt"] = true;
1692
+ await import_fs4.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
1693
+ try {
1694
+ await import_fs4.promises.unlink(statePath);
1695
+ } catch {
1696
+ }
1697
+ outputSpinner.succeed("Output files generated");
1698
+ const capCount = Object.values(manifest.capabilities ?? {}).filter((v) => v === true || typeof v === "string" && v !== "none").length;
1699
+ console.log(`
1700
+ ${import_chalk5.default.bold("OCP Discovery Setup Complete")}
1701
+ ${import_chalk5.default.dim("\u2550".repeat(55))}
1702
+ ${import_chalk5.default.bold("Store:")} ${storeName} (${baseUrl})
1703
+ ${import_chalk5.default.bold("Products:")} ${products.length} extracted from ${urlList.length} crawled URLs
1704
+
1705
+ ${import_chalk5.default.bold("Generated files:")}
1706
+ .well-known/ocp.json ${import_chalk5.default.green("\u2713")} Manifest (${capCount} capabilities)
1707
+ ocp/products.jsonl ${import_chalk5.default.green("\u2713")} Product feed (${products.length} products)
1708
+ ocp.md ${import_chalk5.default.green("\u2713")} Agent storefront context
1709
+ robots.txt ${import_chalk5.default.green("\u2713")} OCP-Manifest directive added
1710
+ llms.txt ${import_chalk5.default.green("\u2713")} Commerce section added
1711
+
1712
+ ${import_chalk5.default.bold("Next steps:")}
1713
+ 1. Review generated files in ${import_chalk5.default.cyan(options.output + "/")}
1714
+ 2. Upload to your web root
1715
+ 3. Verify: ${import_chalk5.default.cyan(`npx @opencommerceprotocol/cli validate ${baseUrl}`)}
1716
+ ${import_chalk5.default.dim("\u2550".repeat(55))}
1717
+ `);
1718
+ }
1719
+ function generateOcpMd2(storeName, storeUrl, description, categories, productCount, priceRange, currency) {
1720
+ return `# ${storeName} \u2014 Open Commerce Protocol
1721
+
1722
+ ## About This Store
1723
+
1724
+ ${description || `${storeName} is an online store located at ${storeUrl}.`}
1725
+
1726
+ ## Catalog Overview
1727
+
1728
+ - **Total Products:** ${productCount}
1729
+ - **Price Range:** ${currency} ${priceRange.min.toFixed(2)} \u2013 ${currency} ${priceRange.max.toFixed(2)}
1730
+ ${categories.length > 0 ? `- **Categories:** ${categories.slice(0, 20).join(", ")}${categories.length > 20 ? ` (+${categories.length - 20} more)` : ""}` : ""}
1731
+
1732
+ ## How to Shop
1733
+
1734
+ 1. Use \`search_products\` to find products by keyword or category
1735
+ 2. Use \`get_product\` to get detailed information on a specific item
1736
+ 3. Use \`add_to_cart\` to add items to the cart
1737
+ 4. Use \`begin_checkout\` to proceed to checkout (a human must complete payment)
1738
+
1739
+ ## Policies
1740
+
1741
+ - All purchases require human confirmation at checkout
1742
+ - Product availability is updated in real-time
1743
+
1744
+ ## Machine-Readable Resources
1745
+
1746
+ - Full llms.txt: /llms.txt
1747
+ - Commerce manifest: /.well-known/ocp.json
1748
+ - Product feed: /ocp/products.jsonl
1749
+ - robots.txt OCP directive: present
1750
+
1751
+ *This document is optimized for AI agents. For the human-readable store, visit ${storeUrl}*
1752
+ `;
1753
+ }
1754
+
1755
+ // src/commands/robots.ts
1756
+ var import_fs5 = require("fs");
1757
+ var import_path5 = __toESM(require("path"));
1758
+ var import_chalk6 = __toESM(require("chalk"));
1759
+ async function runRobots(filePath, options) {
1760
+ const robotsPath = filePath ? import_path5.default.resolve(filePath) : import_path5.default.resolve("robots.txt");
1761
+ let manifestUrl = options.url || "/.well-known/ocp.json";
1762
+ if (!options.url) {
1763
+ try {
1764
+ const manifestPath = import_path5.default.resolve(import_path5.default.dirname(robotsPath), ".well-known", "ocp.json");
1765
+ const manifestRaw = await import_fs5.promises.readFile(manifestPath, "utf-8");
1766
+ const manifest = JSON.parse(manifestRaw);
1767
+ if (manifest.merchant?.url) {
1768
+ manifestUrl = `${manifest.merchant.url.replace(/\/$/, "")}/.well-known/ocp.json`;
1769
+ }
1770
+ } catch {
1771
+ }
1772
+ }
1773
+ let content = "";
1774
+ let existed = false;
1775
+ try {
1776
+ content = await import_fs5.promises.readFile(robotsPath, "utf-8");
1777
+ existed = true;
1778
+ } catch {
1779
+ }
1780
+ const parsed = parseRobotsTxt(content);
1781
+ if (parsed.ocpManifest) {
1782
+ console.log(import_chalk6.default.green("\u2713 robots.txt already contains OCP-Manifest directive"));
1783
+ console.log(import_chalk6.default.dim(` OCP-Manifest: ${parsed.ocpManifest}`));
1784
+ return;
1785
+ }
1786
+ const patched = patchRobotsTxt(content, manifestUrl);
1787
+ if (options.stdout) {
1788
+ console.log(patched);
1789
+ return;
1790
+ }
1791
+ await import_fs5.promises.writeFile(robotsPath, patched, "utf-8");
1792
+ if (existed) {
1793
+ console.log(import_chalk6.default.green("\u2713 Updated robots.txt with OCP-Manifest directive"));
1794
+ } else {
1795
+ console.log(import_chalk6.default.green("\u2713 Created robots.txt with OCP-Manifest directive"));
1796
+ }
1797
+ console.log(import_chalk6.default.dim(` Added: OCP-Manifest: ${manifestUrl}`));
1798
+ }
1799
+
1800
+ // src/commands/llms.ts
1801
+ var import_fs6 = require("fs");
1802
+ var import_path6 = __toESM(require("path"));
1803
+ var import_chalk7 = __toESM(require("chalk"));
1804
+ async function runLlms(filePath, options) {
1805
+ const llmsPath = filePath ? import_path6.default.resolve(filePath) : import_path6.default.resolve("llms.txt");
1806
+ const dir = import_path6.default.dirname(llmsPath);
1807
+ let manifest;
1808
+ try {
1809
+ const manifestPath = import_path6.default.resolve(dir, ".well-known", "ocp.json");
1810
+ const manifestRaw = await import_fs6.promises.readFile(manifestPath, "utf-8");
1811
+ manifest = JSON.parse(manifestRaw);
1812
+ } catch {
1813
+ try {
1814
+ const manifestPath = import_path6.default.resolve(".well-known", "ocp.json");
1815
+ const manifestRaw = await import_fs6.promises.readFile(manifestPath, "utf-8");
1816
+ manifest = JSON.parse(manifestRaw);
1817
+ } catch {
1818
+ }
1819
+ }
1820
+ const commerceData = {
1821
+ manifestUrl: "/.well-known/ocp.json",
1822
+ feedUrl: manifest?.discovery?.feed ? new URL(manifest.discovery.feed).pathname : "/ocp/products.jsonl",
1823
+ ocpMdUrl: "/ocp.md",
1824
+ productCount: manifest?.discovery?.total_products
1825
+ };
1826
+ let existingContent;
1827
+ try {
1828
+ existingContent = await import_fs6.promises.readFile(llmsPath, "utf-8");
1829
+ } catch {
1830
+ }
1831
+ let result;
1832
+ if (existingContent) {
1833
+ const parsed = parseLlmsTxt(existingContent);
1834
+ const hasCommerce = parsed.sections.some((s) => s.heading.toLowerCase() === "commerce");
1835
+ result = patchLlmsTxt(existingContent, commerceData);
1836
+ if (options.stdout) {
1837
+ console.log(result);
1838
+ return;
1839
+ }
1840
+ await import_fs6.promises.writeFile(llmsPath, result, "utf-8");
1841
+ if (hasCommerce) {
1842
+ console.log(import_chalk7.default.green("\u2713 Updated ## Commerce section in llms.txt"));
1843
+ } else {
1844
+ console.log(import_chalk7.default.green("\u2713 Added ## Commerce section to llms.txt"));
1845
+ }
1846
+ } else {
1847
+ let storeDescription = "";
1848
+ try {
1849
+ const ocpMd = await import_fs6.promises.readFile(import_path6.default.resolve(dir, "ocp.md"), "utf-8");
1850
+ const lines = ocpMd.split("\n");
1851
+ let foundHeading = false;
1852
+ for (const line of lines) {
1853
+ if (line.startsWith("#")) {
1854
+ if (foundHeading) break;
1855
+ foundHeading = true;
1856
+ continue;
1857
+ }
1858
+ if (foundHeading && line.trim()) {
1859
+ storeDescription = line.trim();
1860
+ break;
1861
+ }
1862
+ }
1863
+ } catch {
1864
+ }
1865
+ const storeName = manifest?.merchant?.name || "My Store";
1866
+ storeDescription = storeDescription || `Online store at ${manifest?.merchant?.url || "https://mystore.com"}`;
1867
+ result = generateLlmsTxt(storeName, storeDescription, commerceData);
1868
+ if (options.stdout) {
1869
+ console.log(result);
1870
+ return;
1871
+ }
1872
+ await import_fs6.promises.writeFile(llmsPath, result, "utf-8");
1873
+ console.log(import_chalk7.default.green("\u2713 Generated llms.txt with ## Commerce section"));
1874
+ }
1875
+ if (options.full && manifest?.discovery?.feed) {
1876
+ try {
1877
+ const feedPath = import_path6.default.resolve(dir, "ocp", "products.jsonl");
1878
+ const feedRaw = await import_fs6.promises.readFile(feedPath, "utf-8");
1879
+ const products = feedRaw.split("\n").filter((l) => l.trim()).map((l) => {
1880
+ try {
1881
+ return JSON.parse(l);
1882
+ } catch {
1883
+ return null;
1884
+ }
1885
+ }).filter(Boolean);
1886
+ if (products.length > 500) {
1887
+ console.log(import_chalk7.default.yellow(`\u26A0 Catalog has ${products.length} products \u2014 llms-full.txt is capped at 500. Use JSONL feed for full catalog.`));
1888
+ }
1889
+ const fullProducts = products.slice(0, 500);
1890
+ let fullContent = result + "\n\n## Products\n\n";
1891
+ for (const p of fullProducts) {
1892
+ fullContent += `### ${p["name"]}
1893
+ `;
1894
+ if (p["description"]) fullContent += `${p["description"]}
1895
+ `;
1896
+ fullContent += `- Price: ${p["currency"]} ${p["price"]}
1897
+ `;
1898
+ if (p["category"]) fullContent += `- Category: ${p["category"]}
1899
+ `;
1900
+ if (p["in_stock"] !== void 0) fullContent += `- In Stock: ${p["in_stock"] ? "Yes" : "No"}
1901
+ `;
1902
+ if (p["url"]) fullContent += `- URL: ${p["url"]}
1903
+ `;
1904
+ fullContent += "\n";
1905
+ }
1906
+ const fullPath = import_path6.default.resolve(dir, "llms-full.txt");
1907
+ await import_fs6.promises.writeFile(fullPath, fullContent, "utf-8");
1908
+ console.log(import_chalk7.default.green(`\u2713 Generated llms-full.txt (${fullProducts.length} products)`));
1909
+ } catch {
1910
+ console.log(import_chalk7.default.yellow("\u26A0 Could not generate llms-full.txt \u2014 product feed not found"));
1911
+ }
1912
+ }
1913
+ console.log(import_chalk7.default.dim(` File: ${llmsPath}`));
1914
+ }
1915
+
1916
+ // src/commands/stats.ts
1917
+ var import_chalk8 = __toESM(require("chalk"));
1918
+ var import_fs7 = require("fs");
1919
+ var AGENT_UA_PATTERNS = [
1920
+ /GPTBot/i,
1921
+ /Claude-Web/i,
1922
+ /anthropic/i,
1923
+ /OpenAI/i,
1924
+ /Google-Extended/i,
1925
+ /Googlebot/i,
1926
+ /Bingbot/i,
1927
+ /OCPBot/i,
1928
+ /PerplexityBot/i,
1929
+ /YouBot/i,
1930
+ /CCBot/i,
1931
+ /ChatGPT/i,
1932
+ /cohere-ai/i
1933
+ ];
1934
+ var OCP_PATH_PATTERNS = {
1935
+ manifest: /\/.well-known\/ocp\.json/,
1936
+ feed: /\/ocp\/products\.jsonl/,
1937
+ ocpMd: /\/ocp\.md/,
1938
+ toolCall: /\/ocp\/(search|product|cart|checkout)/i
1939
+ };
1940
+ function isAgentUA(ua) {
1941
+ return AGENT_UA_PATTERNS.some((p) => p.test(ua));
1942
+ }
1943
+ function parseNginxLine(line) {
1944
+ const match = line.match(
1945
+ /^\S+ \S+ \S+ \[[^\]]+\] "(?:GET|POST|HEAD|PUT|DELETE|OPTIONS|PATCH) ([^ "]+)[^"]*" (\d+) \d+(?:\s+"[^"]*"\s+"([^"]*)")?/
1946
+ );
1947
+ if (!match) return null;
1948
+ return {
1949
+ path: match[1] ?? "/",
1950
+ status: parseInt(match[2] ?? "0", 10),
1951
+ ua: match[3] ?? ""
1952
+ };
1953
+ }
1954
+ function parseApacheLine(line) {
1955
+ return parseNginxLine(line);
1956
+ }
1957
+ function parseCloudflareLine(line, headerMap) {
1958
+ if (Object.keys(headerMap).length === 0) return null;
1959
+ const fields = splitCsvLine(line);
1960
+ const uriIdx = headerMap["ClientRequestURI"] ?? headerMap["ClientRequestPath"];
1961
+ const uaIdx = headerMap["ClientRequestUserAgent"];
1962
+ const statusIdx = headerMap["EdgeResponseStatus"] ?? headerMap["CacheResponseStatus"];
1963
+ if (uriIdx === void 0 || uaIdx === void 0) return null;
1964
+ const path7 = fields[uriIdx] ?? "/";
1965
+ const ua = fields[uaIdx] ?? "";
1966
+ const status = statusIdx !== void 0 ? parseInt(fields[statusIdx] ?? "0", 10) : 200;
1967
+ return { path: path7.split("?")[0] ?? path7, ua, status };
1968
+ }
1969
+ function splitCsvLine(line) {
1970
+ const fields = [];
1971
+ let current = "";
1972
+ let inQuotes = false;
1973
+ for (let i = 0; i < line.length; i++) {
1974
+ const ch = line[i];
1975
+ if (ch === '"') {
1976
+ if (inQuotes && line[i + 1] === '"') {
1977
+ current += '"';
1978
+ i++;
1979
+ } else {
1980
+ inQuotes = !inQuotes;
1981
+ }
1982
+ } else if (ch === "," && !inQuotes) {
1983
+ fields.push(current);
1984
+ current = "";
1985
+ } else {
1986
+ current += ch;
1987
+ }
1988
+ }
1989
+ fields.push(current);
1990
+ return fields;
1991
+ }
1992
+ function analyzeLines(lines, format) {
1993
+ const stats = {
1994
+ totalRequests: 0,
1995
+ manifestRequests: 0,
1996
+ feedRequests: 0,
1997
+ toolCallRequests: 0,
1998
+ agentUserAgents: {},
1999
+ topPaths: [],
2000
+ conversionFunnel: {
2001
+ manifestViews: 0,
2002
+ feedLoads: 0,
2003
+ productViews: 0,
2004
+ cartAdds: 0,
2005
+ checkouts: 0
2006
+ }
2007
+ };
2008
+ const pathCounts = {};
2009
+ const cloudflareHeaderMap = {};
2010
+ let cloudflareHeaderParsed = false;
2011
+ const parseLine = (line) => {
2012
+ if (format === "cloudflare") {
2013
+ if (!cloudflareHeaderParsed) {
2014
+ const cols = splitCsvLine(line);
2015
+ cols.forEach((col, idx) => {
2016
+ cloudflareHeaderMap[col.trim()] = idx;
2017
+ });
2018
+ cloudflareHeaderParsed = true;
2019
+ return null;
2020
+ }
2021
+ return parseCloudflareLine(line, cloudflareHeaderMap);
2022
+ }
2023
+ return format === "apache" ? parseApacheLine(line) : parseNginxLine(line);
2024
+ };
2025
+ for (const line of lines) {
2026
+ if (!line.trim()) continue;
2027
+ const parsed = parseLine(line);
2028
+ if (!parsed) continue;
2029
+ const { path: path7, ua } = parsed;
2030
+ if (OCP_PATH_PATTERNS.manifest.test(path7)) {
2031
+ stats.manifestRequests++;
2032
+ stats.conversionFunnel.manifestViews++;
2033
+ stats.totalRequests++;
2034
+ } else if (OCP_PATH_PATTERNS.feed.test(path7)) {
2035
+ stats.feedRequests++;
2036
+ stats.conversionFunnel.feedLoads++;
2037
+ stats.totalRequests++;
2038
+ } else if (OCP_PATH_PATTERNS.toolCall.test(path7)) {
2039
+ stats.toolCallRequests++;
2040
+ stats.totalRequests++;
2041
+ if (/\/ocp\/product/i.test(path7)) stats.conversionFunnel.productViews++;
2042
+ if (/\/ocp\/cart/i.test(path7)) stats.conversionFunnel.cartAdds++;
2043
+ if (/\/ocp\/checkout/i.test(path7)) stats.conversionFunnel.checkouts++;
2044
+ } else if (OCP_PATH_PATTERNS.ocpMd.test(path7)) {
2045
+ stats.totalRequests++;
2046
+ }
2047
+ if (isAgentUA(ua)) {
2048
+ const agentName = extractAgentName(ua);
2049
+ stats.agentUserAgents[agentName] = (stats.agentUserAgents[agentName] ?? 0) + 1;
2050
+ }
2051
+ pathCounts[path7] = (pathCounts[path7] ?? 0) + 1;
2052
+ }
2053
+ stats.topPaths = Object.entries(pathCounts).filter(([p]) => p.startsWith("/ocp") || p.includes("ocp.json") || p.includes("ocp.md")).sort(([, a], [, b]) => b - a).slice(0, 10).map(([path7, count]) => ({ path: path7, count }));
2054
+ return stats;
2055
+ }
2056
+ function extractAgentName(ua) {
2057
+ for (const pattern of AGENT_UA_PATTERNS) {
2058
+ const m = ua.match(pattern);
2059
+ if (m) return m[0];
2060
+ }
2061
+ return "Unknown Agent";
2062
+ }
2063
+ function printStats(stats, sourceLabel) {
2064
+ console.log("\n" + import_chalk8.default.bold("OCP Agent Traffic Report"));
2065
+ console.log(import_chalk8.default.dim("\u2501".repeat(50)));
2066
+ console.log(import_chalk8.default.bold("Source:"), sourceLabel);
2067
+ console.log();
2068
+ console.log(import_chalk8.default.bold("OCP Request Summary"));
2069
+ console.log(` Manifest requests: ${import_chalk8.default.cyan(String(stats.manifestRequests))}`);
2070
+ console.log(` Feed loads: ${import_chalk8.default.cyan(String(stats.feedRequests))}`);
2071
+ console.log(` Tool calls: ${import_chalk8.default.cyan(String(stats.toolCallRequests))}`);
2072
+ console.log(` Total OCP requests: ${import_chalk8.default.cyan(String(stats.totalRequests))}`);
2073
+ if (Object.keys(stats.agentUserAgents).length > 0) {
2074
+ console.log();
2075
+ console.log(import_chalk8.default.bold("AI Agents Detected"));
2076
+ const sorted = Object.entries(stats.agentUserAgents).sort(([, a], [, b]) => b - a);
2077
+ for (const [agent, count] of sorted.slice(0, 10)) {
2078
+ console.log(` ${import_chalk8.default.green(agent.padEnd(25))} ${count} requests`);
2079
+ }
2080
+ }
2081
+ console.log();
2082
+ console.log(import_chalk8.default.bold("Conversion Funnel"));
2083
+ const f = stats.conversionFunnel;
2084
+ const funnel = [
2085
+ ["Manifest viewed", f.manifestViews],
2086
+ ["Feed loaded", f.feedLoads],
2087
+ ["Product viewed", f.productViews],
2088
+ ["Cart add", f.cartAdds],
2089
+ ["Checkout started", f.checkouts]
2090
+ ];
2091
+ for (const [label, count] of funnel) {
2092
+ const pct = f.manifestViews > 0 ? Math.round(count / f.manifestViews * 100) : 0;
2093
+ const bar = "\u2588".repeat(Math.max(1, Math.round(pct / 5)));
2094
+ console.log(` ${label.padEnd(20)} ${String(count).padStart(6)} ${import_chalk8.default.dim(bar)} ${pct}%`);
2095
+ }
2096
+ if (stats.topPaths.length > 0) {
2097
+ console.log();
2098
+ console.log(import_chalk8.default.bold("Top OCP Paths"));
2099
+ for (const { path: path7, count } of stats.topPaths) {
2100
+ console.log(` ${String(count).padStart(6)} ${import_chalk8.default.cyan(path7)}`);
2101
+ }
2102
+ }
2103
+ console.log();
2104
+ if (stats.totalRequests === 0) {
2105
+ console.log(import_chalk8.default.yellow("No OCP traffic detected in logs."));
2106
+ console.log(import_chalk8.default.dim("Tip: Ensure your log file covers OCP paths (/.well-known/ocp.json, /ocp/)"));
2107
+ } else {
2108
+ console.log(import_chalk8.default.green(`\u2713 ${stats.totalRequests} OCP requests found across ${Object.keys(stats.agentUserAgents).length} agent types.`));
2109
+ }
2110
+ console.log();
2111
+ }
2112
+ async function runStats(url, options) {
2113
+ const format = options.format ?? "nginx";
2114
+ if (options.log) {
2115
+ let content;
2116
+ try {
2117
+ content = await import_fs7.promises.readFile(options.log, "utf-8");
2118
+ } catch {
2119
+ console.error(import_chalk8.default.red(`Error: Cannot read log file: ${options.log}`));
2120
+ process.exit(1);
2121
+ }
2122
+ const lines = content.split("\n");
2123
+ const stats = analyzeLines(lines, format);
2124
+ printStats(stats, options.log);
2125
+ return;
2126
+ }
2127
+ console.log("\n" + import_chalk8.default.bold("OCP Stats \u2014 Log Analysis"));
2128
+ console.log(import_chalk8.default.dim("\u2501".repeat(50)));
2129
+ console.log();
2130
+ console.log("Analyze your server access logs for OCP agent traffic:");
2131
+ console.log();
2132
+ console.log(import_chalk8.default.cyan(" # Nginx"));
2133
+ console.log(" npx @opencommerceprotocol/cli stats --log /var/log/nginx/access.log");
2134
+ console.log();
2135
+ console.log(import_chalk8.default.cyan(" # Apache"));
2136
+ console.log(" npx @opencommerceprotocol/cli stats --log /var/log/apache2/access.log --format apache");
2137
+ console.log();
2138
+ console.log(import_chalk8.default.cyan(" # Cloudflare (export CSV from dashboard first)"));
2139
+ console.log(" npx @opencommerceprotocol/cli stats --log cloudflare-export.csv --format cloudflare");
2140
+ console.log();
2141
+ console.log(import_chalk8.default.dim("What you'll see:"));
2142
+ console.log(" - Which AI agents are visiting your store");
2143
+ console.log(" - How many times your manifest and feed are fetched");
2144
+ console.log(" - Conversion funnel from discovery to checkout");
2145
+ console.log();
2146
+ if (url) {
2147
+ console.log(import_chalk8.default.yellow(`Tip: Use npx @opencommerceprotocol/cli test-agent ${url} to simulate an agent visit.`));
2148
+ }
2149
+ }
2150
+
2151
+ // src/commands/test-agent.ts
2152
+ var import_chalk9 = __toESM(require("chalk"));
2153
+ var import_validator2 = require("@opencommerceprotocol/validator");
2154
+ function formatCheck(result) {
2155
+ const icon = result.passed ? import_chalk9.default.green(" \u2713") : import_chalk9.default.red(" \u2717");
2156
+ console.log(`${icon} ${result.name}`);
2157
+ if (result.message) {
2158
+ const color = result.passed ? import_chalk9.default.dim : import_chalk9.default.yellow;
2159
+ console.log(` ${color(result.message)}`);
2160
+ }
2161
+ if (result.detail && !result.passed) {
2162
+ console.log(` ${import_chalk9.default.dim(result.detail)}`);
2163
+ }
2164
+ }
2165
+ async function runTestAgent(url, options) {
2166
+ const baseUrl = url.replace(/\/$/, "");
2167
+ const testQuery = options.query ?? "product";
2168
+ console.log("\n" + import_chalk9.default.bold("OCP Agent Simulation"));
2169
+ console.log(import_chalk9.default.dim("\u2501".repeat(50)));
2170
+ console.log(import_chalk9.default.bold("Target:"), baseUrl);
2171
+ console.log(import_chalk9.default.bold("Test query:"), `"${testQuery}"`);
2172
+ console.log();
2173
+ const allResults = [];
2174
+ let manifest = null;
2175
+ let feedUrl = null;
2176
+ let firstProduct = null;
2177
+ console.log(import_chalk9.default.bold("Step 1: Fetching manifest"));
2178
+ const step1 = [];
2179
+ try {
2180
+ const res = await fetch(`${baseUrl}/.well-known/ocp.json`);
2181
+ if (!res.ok) {
2182
+ step1.push({
2183
+ name: "Manifest accessible",
2184
+ passed: false,
2185
+ message: `HTTP ${res.status} \u2014 manifest not found`,
2186
+ detail: "Agents cannot discover this store. Run: npx @opencommerceprotocol/cli init"
2187
+ });
2188
+ } else {
2189
+ manifest = await res.json();
2190
+ step1.push({
2191
+ name: "Manifest accessible",
2192
+ passed: true,
2193
+ message: `Found /.well-known/ocp.json`
2194
+ });
2195
+ const hasVersion = Boolean(manifest["version"]);
2196
+ const hasMerchant = Boolean(manifest["merchant"]);
2197
+ const hasDiscovery = Boolean(manifest["discovery"]);
2198
+ step1.push({
2199
+ name: "Manifest has required fields",
2200
+ passed: hasVersion && hasMerchant,
2201
+ message: hasVersion && hasMerchant ? `version: ${manifest["version"]}, merchant: ${manifest["merchant"]?.["name"]}` : "Missing required fields: version, merchant"
2202
+ });
2203
+ step1.push({
2204
+ name: "Manifest has discovery config",
2205
+ passed: hasDiscovery,
2206
+ message: hasDiscovery ? "discovery section present" : "No discovery section \u2014 agents cannot find your product feed"
2207
+ });
2208
+ if (hasDiscovery) {
2209
+ const discovery = manifest["discovery"];
2210
+ feedUrl = discovery["feed"] ?? `${baseUrl}/ocp/products.jsonl`;
2211
+ if (discovery["feed_updated"]) {
2212
+ const updatedAt = new Date(discovery["feed_updated"]);
2213
+ const ageHours = (Date.now() - updatedAt.getTime()) / 1e3 / 3600;
2214
+ step1.push({
2215
+ name: "Feed freshness (feed_updated)",
2216
+ passed: ageHours < 48,
2217
+ message: ageHours < 1 ? "Feed updated recently (< 1 hour ago)" : ageHours < 24 ? `Feed updated ${Math.round(ageHours)} hours ago` : `Feed is ${Math.round(ageHours / 24)} days old \u2014 consider more frequent updates`
2218
+ });
2219
+ } else {
2220
+ step1.push({
2221
+ name: "Feed freshness (feed_updated)",
2222
+ passed: false,
2223
+ message: "feed_updated not set \u2014 agents cannot assess data freshness",
2224
+ detail: 'Add feed_updated: "<ISO-timestamp>" to your discovery config'
2225
+ });
2226
+ }
2227
+ }
2228
+ }
2229
+ } catch (err) {
2230
+ step1.push({
2231
+ name: "Manifest accessible",
2232
+ passed: false,
2233
+ message: `Network error: ${err instanceof Error ? err.message : String(err)}`
2234
+ });
2235
+ }
2236
+ for (const r of step1) {
2237
+ formatCheck(r);
2238
+ allResults.push(r);
2239
+ }
2240
+ console.log();
2241
+ console.log(import_chalk9.default.bold("Step 2: Loading product feed"));
2242
+ const step2 = [];
2243
+ const effectiveFeedUrl = feedUrl ?? `${baseUrl}/ocp/products.jsonl`;
2244
+ const products = [];
2245
+ try {
2246
+ const res = await fetch(effectiveFeedUrl);
2247
+ if (!res.ok) {
2248
+ step2.push({
2249
+ name: "Product feed accessible",
2250
+ passed: false,
2251
+ message: `HTTP ${res.status} at ${effectiveFeedUrl}`,
2252
+ detail: "Run: npx @opencommerceprotocol/cli generate --products products.csv"
2253
+ });
2254
+ } else {
2255
+ const text = await res.text();
2256
+ const lines = text.split("\n").filter((l) => l.trim()).slice(0, 1e3);
2257
+ let validCount = 0;
2258
+ let invalidCount = 0;
2259
+ let withNotesCount = 0;
2260
+ const parseErrors = [];
2261
+ for (const line of lines) {
2262
+ try {
2263
+ const p = JSON.parse(line);
2264
+ if (p["id"] && p["name"] && p["price"] !== void 0) {
2265
+ validCount++;
2266
+ if (!firstProduct) firstProduct = p;
2267
+ } else {
2268
+ invalidCount++;
2269
+ }
2270
+ if (p["agent_notes"]) withNotesCount++;
2271
+ products.push(p);
2272
+ } catch {
2273
+ invalidCount++;
2274
+ if (parseErrors.length < 3) parseErrors.push(line.slice(0, 60));
2275
+ }
2276
+ }
2277
+ step2.push({
2278
+ name: "Product feed accessible",
2279
+ passed: true,
2280
+ message: `${lines.length} products found at ${effectiveFeedUrl}`
2281
+ });
2282
+ step2.push({
2283
+ name: "Products are valid",
2284
+ passed: invalidCount === 0,
2285
+ message: invalidCount === 0 ? `All ${validCount} products have required fields (id, name, price)` : `${invalidCount} invalid products (missing id, name, or price)`,
2286
+ detail: parseErrors.length > 0 ? `Parse errors: ${parseErrors.join(", ")}` : void 0
2287
+ });
2288
+ const coverage = lines.length > 0 ? Math.round(withNotesCount / lines.length * 100) : 0;
2289
+ step2.push({
2290
+ name: "agent_notes coverage",
2291
+ passed: coverage >= 80,
2292
+ message: `${coverage}% of products have agent_notes (${withNotesCount}/${lines.length})`,
2293
+ detail: coverage < 80 ? "Aim for 100% coverage. Run: npx @opencommerceprotocol/cli generate to auto-generate notes" : void 0
2294
+ });
2295
+ }
2296
+ } catch (err) {
2297
+ step2.push({
2298
+ name: "Product feed accessible",
2299
+ passed: false,
2300
+ message: `Network error: ${err instanceof Error ? err.message : String(err)}`
2301
+ });
2302
+ }
2303
+ for (const r of step2) {
2304
+ formatCheck(r);
2305
+ allResults.push(r);
2306
+ }
2307
+ console.log();
2308
+ console.log(import_chalk9.default.bold("Step 3: Evaluating agent_notes quality"));
2309
+ const step3 = [];
2310
+ if (firstProduct) {
2311
+ const notesQuality = (0, import_validator2.scoreAgentNotes)(
2312
+ firstProduct["agent_notes"],
2313
+ firstProduct["name"]
2314
+ );
2315
+ step3.push({
2316
+ name: "Sample product agent_notes",
2317
+ passed: notesQuality.score >= 70,
2318
+ message: `Score: ${notesQuality.score}/100 \u2014 "${firstProduct["name"]}"`,
2319
+ detail: notesQuality.issues.length > 0 ? `Issues: ${notesQuality.issues.join("; ")}` : void 0
2320
+ });
2321
+ for (const r of step3) {
2322
+ formatCheck(r);
2323
+ allResults.push(r);
2324
+ }
2325
+ if (notesQuality.suggestions.length > 0 && options.verbose) {
2326
+ console.log();
2327
+ console.log(import_chalk9.default.bold(" agent_notes suggestions:"));
2328
+ for (const s of notesQuality.suggestions) {
2329
+ console.log(` ${import_chalk9.default.yellow("\u2192")} ${s}`);
2330
+ }
2331
+ }
2332
+ if (options.verbose && firstProduct["agent_notes"]) {
2333
+ console.log();
2334
+ console.log(import_chalk9.default.dim(" Sample agent_notes:"));
2335
+ console.log(import_chalk9.default.dim(` "${firstProduct["agent_notes"]}"`));
2336
+ }
2337
+ } else {
2338
+ console.log(import_chalk9.default.dim(" (skipped \u2014 no products loaded)"));
2339
+ }
2340
+ console.log();
2341
+ console.log(import_chalk9.default.bold("Step 4: Checking store description (ocp.md)"));
2342
+ const step4 = [];
2343
+ try {
2344
+ const res = await fetch(`${baseUrl}/ocp.md`);
2345
+ if (!res.ok) {
2346
+ step4.push({
2347
+ name: "ocp.md accessible",
2348
+ passed: false,
2349
+ message: "Not found \u2014 agents lack store context",
2350
+ detail: "Add /ocp.md with a natural language description of your store"
2351
+ });
2352
+ } else {
2353
+ const text = await res.text();
2354
+ const hasHeadings = text.includes("#");
2355
+ const isLong = text.length > 200;
2356
+ step4.push({
2357
+ name: "ocp.md accessible",
2358
+ passed: true,
2359
+ message: `Found (${text.length} chars)`
2360
+ });
2361
+ step4.push({
2362
+ name: "ocp.md well-formatted",
2363
+ passed: hasHeadings && isLong,
2364
+ message: hasHeadings && isLong ? "Has headings and sufficient content" : "Minimal content \u2014 expand with store description, policies, and categories"
2365
+ });
2366
+ }
2367
+ } catch {
2368
+ step4.push({
2369
+ name: "ocp.md accessible",
2370
+ passed: false,
2371
+ message: "Network error"
2372
+ });
2373
+ }
2374
+ for (const r of step4) {
2375
+ formatCheck(r);
2376
+ allResults.push(r);
2377
+ }
2378
+ console.log();
2379
+ console.log(import_chalk9.default.bold("Step 5: Checking discovery signals"));
2380
+ const step5 = [];
2381
+ try {
2382
+ const res = await fetch(`${baseUrl}/robots.txt`);
2383
+ if (res.ok) {
2384
+ const text = await res.text();
2385
+ const hasOcp = /OCP-Manifest:/i.test(text);
2386
+ step5.push({
2387
+ name: "robots.txt OCP-Manifest directive",
2388
+ passed: hasOcp,
2389
+ message: hasOcp ? "OCP-Manifest directive found" : "Missing \u2014 run: npx @opencommerceprotocol/cli robots"
2390
+ });
2391
+ } else {
2392
+ step5.push({
2393
+ name: "robots.txt OCP-Manifest directive",
2394
+ passed: false,
2395
+ message: "robots.txt not found"
2396
+ });
2397
+ }
2398
+ } catch {
2399
+ step5.push({
2400
+ name: "robots.txt OCP-Manifest directive",
2401
+ passed: false,
2402
+ message: "Could not fetch robots.txt"
2403
+ });
2404
+ }
2405
+ try {
2406
+ const res = await fetch(`${baseUrl}/llms.txt`);
2407
+ if (res.ok) {
2408
+ const text = await res.text();
2409
+ const hasCommerce = text.includes("## Commerce");
2410
+ step5.push({
2411
+ name: "llms.txt ## Commerce section",
2412
+ passed: hasCommerce,
2413
+ message: hasCommerce ? "## Commerce section found" : "Missing Commerce section \u2014 run: npx @opencommerceprotocol/cli llms"
2414
+ });
2415
+ } else {
2416
+ step5.push({
2417
+ name: "llms.txt ## Commerce section",
2418
+ passed: false,
2419
+ message: "llms.txt not found \u2014 run: npx @opencommerceprotocol/cli llms"
2420
+ });
2421
+ }
2422
+ } catch {
2423
+ step5.push({
2424
+ name: "llms.txt ## Commerce section",
2425
+ passed: false,
2426
+ message: "Could not fetch llms.txt"
2427
+ });
2428
+ }
2429
+ for (const r of step5) {
2430
+ formatCheck(r);
2431
+ allResults.push(r);
2432
+ }
2433
+ const totalChecks = allResults.length;
2434
+ const passedChecks = allResults.filter((r) => r.passed).length;
2435
+ const failedChecks = totalChecks - passedChecks;
2436
+ const score = totalChecks > 0 ? Math.round(passedChecks / totalChecks * 100) : 0;
2437
+ console.log();
2438
+ console.log(import_chalk9.default.dim("\u2501".repeat(50)));
2439
+ console.log();
2440
+ const scoreColor2 = score === 100 ? import_chalk9.default.green : score >= 70 ? import_chalk9.default.yellow : import_chalk9.default.red;
2441
+ console.log(
2442
+ import_chalk9.default.bold("Result: ") + scoreColor2(`${passedChecks}/${totalChecks} checks passed`) + import_chalk9.default.dim(` (${score}/100)`)
2443
+ );
2444
+ if (failedChecks > 0) {
2445
+ console.log();
2446
+ console.log(import_chalk9.default.bold("Failed checks:"));
2447
+ for (const r of allResults.filter((r2) => !r2.passed)) {
2448
+ console.log(` ${import_chalk9.default.red("\u2717")} ${r.name}: ${import_chalk9.default.yellow(r.message)}`);
2449
+ }
2450
+ }
2451
+ console.log();
2452
+ if (score === 100) {
2453
+ console.log(import_chalk9.default.green("\u2713 All checks passed. This store is fully agent-ready."));
2454
+ } else if (score >= 70) {
2455
+ console.log(import_chalk9.default.yellow("\u26A0 Store is mostly agent-ready. Fix the failing checks above."));
2456
+ } else {
2457
+ console.log(import_chalk9.default.red("\u2717 Store needs significant work to be agent-ready. See checks above."));
2458
+ }
2459
+ console.log();
2460
+ console.log(import_chalk9.default.dim("Next steps:"));
2461
+ console.log(` ${import_chalk9.default.cyan("npx @opencommerceprotocol/cli validate")} ${baseUrl} \u2014 full validation report with score`);
2462
+ console.log(` ${import_chalk9.default.cyan("npx @opencommerceprotocol/cli stats")} --log /var/log/nginx/access.log \u2014 see real agent traffic`);
2463
+ console.log();
2464
+ }
2465
+
2466
+ // src/commands/preview.ts
2467
+ var import_chalk10 = __toESM(require("chalk"));
2468
+ function truncate(str, maxLen) {
2469
+ if (str.length <= maxLen) return str;
2470
+ return str.slice(0, maxLen - 1) + "\u2026";
2471
+ }
2472
+ function renderAvailability(product) {
2473
+ if (product["availability"]) {
2474
+ const a = product["availability"];
2475
+ const colors = {
2476
+ in_stock: import_chalk10.default.green,
2477
+ out_of_stock: import_chalk10.default.red,
2478
+ limited: import_chalk10.default.yellow,
2479
+ pre_order: import_chalk10.default.cyan,
2480
+ backorder: import_chalk10.default.yellow,
2481
+ check_site: import_chalk10.default.dim
2482
+ };
2483
+ return (colors[a] ?? import_chalk10.default.dim)(a.replace(/_/g, " "));
2484
+ }
2485
+ if (typeof product["in_stock"] === "boolean") {
2486
+ return product["in_stock"] ? import_chalk10.default.green("in stock") : import_chalk10.default.red("out of stock");
2487
+ }
2488
+ return import_chalk10.default.dim("unknown");
2489
+ }
2490
+ async function runPreview(url, options) {
2491
+ const baseUrl = url.replace(/\/$/, "");
2492
+ const maxProducts = options.products ?? 5;
2493
+ console.log("\n" + import_chalk10.default.bold("OCP Agent Preview"));
2494
+ console.log(import_chalk10.default.dim("What an AI agent sees when it visits your store"));
2495
+ console.log(import_chalk10.default.dim("\u2501".repeat(60)));
2496
+ console.log();
2497
+ let manifest = null;
2498
+ try {
2499
+ const res = await fetch(`${baseUrl}/.well-known/ocp.json`);
2500
+ if (res.ok) {
2501
+ manifest = await res.json();
2502
+ }
2503
+ } catch {
2504
+ }
2505
+ if (!manifest) {
2506
+ console.log(import_chalk10.default.red("\u2717 No manifest found at /.well-known/ocp.json"));
2507
+ console.log(import_chalk10.default.dim(" Run: npx @opencommerceprotocol/cli init"));
2508
+ return;
2509
+ }
2510
+ const merchant = manifest["merchant"];
2511
+ const capabilities = manifest["capabilities"];
2512
+ const discovery = manifest["discovery"];
2513
+ const bridge = manifest["bridge"];
2514
+ console.log(import_chalk10.default.bold.cyan("\u2500\u2500 STORE IDENTITY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2515
+ console.log(import_chalk10.default.bold("Name: ") + (merchant?.["name"] ?? import_chalk10.default.dim("(not set)")));
2516
+ console.log(import_chalk10.default.bold("URL: ") + (merchant?.["url"] ?? import_chalk10.default.dim("(not set)")));
2517
+ console.log(import_chalk10.default.bold("Currency:") + " " + (merchant?.["currency"] ?? import_chalk10.default.dim("(not set)")));
2518
+ if (merchant?.["description"]) {
2519
+ console.log(import_chalk10.default.bold("About: ") + truncate(String(merchant["description"]), 120));
2520
+ }
2521
+ console.log();
2522
+ console.log(import_chalk10.default.bold.cyan("\u2500\u2500 CAPABILITIES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2523
+ const caps = [
2524
+ ["catalog", "Browse product catalog"],
2525
+ ["search", "Search products"],
2526
+ ["cart", "Add to cart"],
2527
+ ["checkout", "Initiate checkout"]
2528
+ ];
2529
+ for (const [key, label] of caps) {
2530
+ const enabled = Boolean(capabilities?.[key]);
2531
+ console.log(` ${enabled ? import_chalk10.default.green("\u2713") : import_chalk10.default.dim("\u25CB")} ${label}`);
2532
+ }
2533
+ if (bridge && Object.values(bridge).some(Boolean)) {
2534
+ console.log();
2535
+ console.log(import_chalk10.default.bold.cyan("\u2500\u2500 PROTOCOL BRIDGES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2536
+ const protocols = ["mcp", "ucp", "acp", "a2a"];
2537
+ for (const proto of protocols) {
2538
+ if (bridge[proto]) {
2539
+ console.log(` ${import_chalk10.default.green("\u2713")} ${proto.toUpperCase()}: ${import_chalk10.default.dim(String(bridge[proto]))}`);
2540
+ } else {
2541
+ console.log(` ${import_chalk10.default.dim("\u25CB")} ${proto.toUpperCase()} ${import_chalk10.default.dim("(not configured)")}`);
2542
+ }
2543
+ }
2544
+ }
2545
+ console.log();
2546
+ console.log(import_chalk10.default.bold.cyan("\u2500\u2500 DISCOVERY SIGNALS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2547
+ if (discovery) {
2548
+ const feedUrl2 = discovery["feed"] ?? `${baseUrl}/ocp/products.jsonl`;
2549
+ const feedUpdated = discovery["feed_updated"];
2550
+ const totalProducts = discovery["total_products"];
2551
+ console.log(` ${import_chalk10.default.green("\u2713")} Feed: ${import_chalk10.default.dim(feedUrl2)}`);
2552
+ if (totalProducts) {
2553
+ console.log(` ${import_chalk10.default.dim(`${totalProducts} products`)}`);
2554
+ }
2555
+ if (feedUpdated) {
2556
+ const age = Math.round((Date.now() - new Date(feedUpdated).getTime()) / 1e3 / 3600);
2557
+ const ageStr = age < 1 ? "just now" : age < 24 ? `${age}h ago` : `${Math.round(age / 24)}d ago`;
2558
+ const ageColor = age < 48 ? import_chalk10.default.green : import_chalk10.default.yellow;
2559
+ console.log(` Last updated: ${ageColor(ageStr)}`);
2560
+ } else {
2561
+ console.log(` ${import_chalk10.default.yellow("feed_updated not set \u2014 agents cannot assess freshness")}`);
2562
+ }
2563
+ if (discovery["feed_generated_from"]) {
2564
+ console.log(` Generated from: ${import_chalk10.default.dim(String(discovery["feed_generated_from"]))}`);
2565
+ }
2566
+ } else {
2567
+ console.log(` ${import_chalk10.default.yellow("No discovery section in manifest")}`);
2568
+ }
2569
+ console.log();
2570
+ console.log(import_chalk10.default.bold.cyan("\u2500\u2500 STORE DESCRIPTION (ocp.md) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2571
+ try {
2572
+ const res = await fetch(`${baseUrl}/ocp.md`);
2573
+ if (res.ok) {
2574
+ const text = await res.text();
2575
+ const lines = text.split("\n").slice(0, options.verbose ? 30 : 15);
2576
+ for (const line of lines) {
2577
+ if (line.startsWith("# ")) {
2578
+ console.log(import_chalk10.default.bold(" " + line));
2579
+ } else if (line.startsWith("## ")) {
2580
+ console.log(import_chalk10.default.cyan(" " + line));
2581
+ } else {
2582
+ console.log(import_chalk10.default.dim(" " + line));
2583
+ }
2584
+ }
2585
+ if (!options.verbose && text.split("\n").length > 15) {
2586
+ console.log(import_chalk10.default.dim(` \u2026 (${text.split("\n").length - 15} more lines, use --verbose to see all)`));
2587
+ }
2588
+ } else {
2589
+ console.log(import_chalk10.default.yellow(" Not found \u2014 agents lack store context"));
2590
+ console.log(import_chalk10.default.dim(" Add /ocp.md with a natural language store description"));
2591
+ }
2592
+ } catch {
2593
+ console.log(import_chalk10.default.dim(" Could not fetch ocp.md"));
2594
+ }
2595
+ console.log();
2596
+ console.log(import_chalk10.default.bold.cyan(`\u2500\u2500 PRODUCT SAMPLE (first ${maxProducts}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
2597
+ const feedUrl = discovery?.["feed"] ?? `${baseUrl}/ocp/products.jsonl`;
2598
+ try {
2599
+ const res = await fetch(feedUrl);
2600
+ if (!res.ok) {
2601
+ console.log(import_chalk10.default.yellow(` Feed not accessible at ${feedUrl}`));
2602
+ } else {
2603
+ const text = await res.text();
2604
+ const lines = text.split("\n").filter((l) => l.trim());
2605
+ const products = [];
2606
+ for (const line of lines.slice(0, Math.min(lines.length, 500))) {
2607
+ try {
2608
+ products.push(JSON.parse(line));
2609
+ } catch {
2610
+ }
2611
+ }
2612
+ console.log(import_chalk10.default.dim(` ${lines.length} total products in feed`));
2613
+ console.log();
2614
+ const sample = products.slice(0, maxProducts);
2615
+ for (const p of sample) {
2616
+ const price = `${p["currency"] ?? "USD"} ${Number(p["price"]).toFixed(2)}`;
2617
+ const avail = renderAvailability(p);
2618
+ console.log(` ${import_chalk10.default.bold(String(p["name"]))} ${import_chalk10.default.dim(`(${p["id"]})`)} \u2014 ${price} \u2014 ${avail}`);
2619
+ if (p["agent_notes"]) {
2620
+ const notes = String(p["agent_notes"]);
2621
+ const lengthColor = notes.length < 50 ? import_chalk10.default.red : notes.length < 100 ? import_chalk10.default.yellow : import_chalk10.default.green;
2622
+ console.log(` ${import_chalk10.default.dim("notes:")} ${lengthColor(truncate(notes, 120))}`);
2623
+ } else {
2624
+ console.log(` ${import_chalk10.default.red("notes: (missing)")}`);
2625
+ }
2626
+ console.log();
2627
+ }
2628
+ console.log(import_chalk10.default.bold.cyan("\u2500\u2500 AGENT_NOTES QUALITY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2629
+ const withNotes = products.filter((p) => p["agent_notes"]).length;
2630
+ const coverage = Math.round(withNotes / products.length * 100);
2631
+ const shortNotes = products.filter(
2632
+ (p) => p["agent_notes"] && String(p["agent_notes"]).length < 50
2633
+ ).length;
2634
+ const missingNotes = products.length - withNotes;
2635
+ const coverageColor = coverage >= 80 ? import_chalk10.default.green : coverage >= 50 ? import_chalk10.default.yellow : import_chalk10.default.red;
2636
+ console.log(` Coverage: ${coverageColor(`${coverage}%`)} (${withNotes}/${products.length} products)`);
2637
+ if (missingNotes > 0) {
2638
+ console.log(` ${import_chalk10.default.red("Missing:")} ${missingNotes} products have no agent_notes`);
2639
+ }
2640
+ if (shortNotes > 0) {
2641
+ console.log(` ${import_chalk10.default.yellow("Too short:")} ${shortNotes} products have notes under 50 chars`);
2642
+ }
2643
+ if (coverage === 100 && shortNotes === 0) {
2644
+ console.log(` ${import_chalk10.default.green("\u2713 All products have adequate agent_notes")}`);
2645
+ }
2646
+ const sorted = products.filter((p) => p["agent_notes"]).sort((a, b) => String(b["agent_notes"]).length - String(a["agent_notes"]).length);
2647
+ if (sorted.length > 0 && options.verbose) {
2648
+ console.log();
2649
+ console.log(import_chalk10.default.bold(" Best agent_notes:"));
2650
+ const best = sorted[0];
2651
+ console.log(` ${import_chalk10.default.dim(String(best["name"]))}`);
2652
+ console.log(` "${import_chalk10.default.green(truncate(String(best["agent_notes"]), 200))}"`);
2653
+ if (sorted.length > 1) {
2654
+ console.log();
2655
+ console.log(import_chalk10.default.bold(" Worst agent_notes:"));
2656
+ const worst = sorted[sorted.length - 1];
2657
+ console.log(` ${import_chalk10.default.dim(String(worst["name"]))}`);
2658
+ console.log(` "${import_chalk10.default.yellow(truncate(String(worst["agent_notes"]), 200))}"`);
2659
+ }
2660
+ }
2661
+ }
2662
+ } catch (err) {
2663
+ console.log(import_chalk10.default.yellow(` Could not load product feed: ${err instanceof Error ? err.message : String(err)}`));
2664
+ }
2665
+ console.log();
2666
+ console.log(import_chalk10.default.dim("\u2501".repeat(60)));
2667
+ console.log(import_chalk10.default.dim("Run `npx @opencommerceprotocol/cli validate " + baseUrl + "` for a full scored report."));
2668
+ console.log(import_chalk10.default.dim("Run `npx @opencommerceprotocol/cli test-agent " + baseUrl + "` to simulate an agent visit."));
2669
+ console.log();
2670
+ }
2671
+
2672
+ // src/commands/agent-discover.ts
2673
+ var import_chalk11 = __toESM(require("chalk"));
2674
+ async function runAgentDiscover(url, options = {}) {
2675
+ const { default: ora } = await import("ora");
2676
+ const spinner = ora(`Discovering agents at ${import_chalk11.default.cyan(url)}`).start();
2677
+ const base = url.replace(/\/$/, "");
2678
+ let manifest = null;
2679
+ let manifestUrl = null;
2680
+ let mode = "none";
2681
+ const candidates = [
2682
+ `${base}/.well-known/agent.json`,
2683
+ `${base}/.well-known/agent-tools`
2684
+ ];
2685
+ for (const candidateUrl of candidates) {
2686
+ try {
2687
+ const res = await fetchWithTimeout(candidateUrl, 8e3);
2688
+ if (res.ok) {
2689
+ const data = await res.json();
2690
+ if (data && typeof data.name === "string" && Array.isArray(data.tools)) {
2691
+ manifest = data;
2692
+ manifestUrl = candidateUrl;
2693
+ mode = "direct";
2694
+ break;
2695
+ }
2696
+ }
2697
+ } catch {
2698
+ }
2699
+ }
2700
+ if (!manifest) {
2701
+ try {
2702
+ const ocpUrl = `${base}/.well-known/ocp.json`;
2703
+ const res = await fetchWithTimeout(ocpUrl, 8e3);
2704
+ if (res.ok) {
2705
+ const ocp = await res.json();
2706
+ if (ocp && typeof ocp["merchant"]?.["name"] === "string") {
2707
+ manifest = convertOCPToAgentManifest(ocp, base);
2708
+ manifestUrl = ocpUrl;
2709
+ mode = "ocp_fallback";
2710
+ }
2711
+ }
2712
+ } catch {
2713
+ }
2714
+ }
2715
+ spinner.stop();
2716
+ if (options.format === "json") {
2717
+ if (manifest) {
2718
+ console.log(JSON.stringify(manifest, null, 2));
2719
+ } else {
2720
+ console.log(JSON.stringify({ found: false, url: base }, null, 2));
2721
+ }
2722
+ return;
2723
+ }
2724
+ if (options.format === "mcp" && manifest) {
2725
+ const mcpTools = toMCPTools(manifest);
2726
+ console.log(JSON.stringify(mcpTools, null, 2));
2727
+ return;
2728
+ }
2729
+ if (options.format === "openai" && manifest) {
2730
+ const openaiTools = toOpenAITools(manifest);
2731
+ console.log(JSON.stringify(openaiTools, null, 2));
2732
+ return;
2733
+ }
2734
+ console.log("\n" + import_chalk11.default.bold("Agent Discovery Report"));
2735
+ console.log(import_chalk11.default.dim("\u2501".repeat(52)));
2736
+ console.log(import_chalk11.default.bold("URL:"), base);
2737
+ console.log();
2738
+ if (!manifest) {
2739
+ check2(false, "Discovery endpoint not found");
2740
+ console.log();
2741
+ console.log(import_chalk11.default.yellow(" \u2192 Add /.well-known/agent.json to make your site agent-discoverable"));
2742
+ console.log(import_chalk11.default.yellow(" \u2192 Or add /.well-known/ocp.json for OCP-compatible discovery"));
2743
+ console.log();
2744
+ return;
2745
+ }
2746
+ if (mode === "direct") {
2747
+ check2(true, `Discovery endpoint found: ${import_chalk11.default.dim(manifestUrl ?? "")}`);
2748
+ } else {
2749
+ check2(true, `OCP fallback discovery: ${import_chalk11.default.dim(manifestUrl ?? "")}`);
2750
+ console.log(import_chalk11.default.dim(" \u2139 To enable direct discovery, add /.well-known/agent.json"));
2751
+ }
2752
+ const tools = manifest.tools ?? [];
2753
+ check2(tools.length > 0, `${tools.length} tool${tools.length === 1 ? "" : "s"} declared`);
2754
+ let allToolsValid = true;
2755
+ for (const tool of tools) {
2756
+ if (!tool.name || !tool.endpoint) {
2757
+ allToolsValid = false;
2758
+ break;
2759
+ }
2760
+ }
2761
+ check2(allToolsValid, allToolsValid ? "All tools have name and endpoint" : "Some tools missing required fields");
2762
+ const toolsWithSchema = tools.filter((t) => t.parameters ?? t.schema);
2763
+ const schemaValid = toolsWithSchema.length > 0;
2764
+ check2(
2765
+ schemaValid,
2766
+ schemaValid ? `${toolsWithSchema.length}/${tools.length} tools have schema definitions` : "No tool schema definitions found (agents may not know what to send)"
2767
+ );
2768
+ const hasAuth = !!manifest.auth;
2769
+ check2(hasAuth, hasAuth ? `Auth: ${manifest.auth?.type ?? "declared"}` : 'Auth not declared (add auth.type = "none" if public)');
2770
+ const bridge = manifest.bridge;
2771
+ if (bridge) {
2772
+ console.log();
2773
+ console.log(import_chalk11.default.bold("Protocol Bridges"));
2774
+ check2(!!bridge["mcp"], bridge["mcp"] ? `MCP: ${bridge["mcp"]}` : "MCP bridge not configured");
2775
+ check2(!!bridge["openai"], bridge["openai"] ? `OpenAI: ${bridge["openai"]}` : "OpenAI bridge not configured");
2776
+ check2(!!bridge["a2a"], bridge["a2a"] ? `A2A: ${bridge["a2a"]}` : "A2A bridge not configured");
2777
+ }
2778
+ console.log();
2779
+ console.log(import_chalk11.default.bold("Manifest Details"));
2780
+ console.log(` ${import_chalk11.default.dim("Name:")} ${manifest.name ?? import_chalk11.default.dim("(not set)")}`);
2781
+ if (manifest.description) {
2782
+ console.log(` ${import_chalk11.default.dim("Description:")} ${manifest.description}`);
2783
+ }
2784
+ if (manifest.verticals?.length) {
2785
+ console.log(` ${import_chalk11.default.dim("Verticals:")} ${manifest.verticals.join(", ")}`);
2786
+ }
2787
+ if (manifest.tool_format) {
2788
+ console.log(` ${import_chalk11.default.dim("Tool format:")} ${manifest.tool_format}`);
2789
+ }
2790
+ if (manifest.updated_at) {
2791
+ console.log(` ${import_chalk11.default.dim("Updated:")} ${manifest.updated_at}`);
2792
+ }
2793
+ if (options.verbose) {
2794
+ console.log();
2795
+ console.log(import_chalk11.default.bold("Tools"));
2796
+ for (const tool of tools) {
2797
+ console.log(` ${import_chalk11.default.cyan(tool.name ?? "(unnamed)")} ${import_chalk11.default.dim(tool.method ?? "POST")} ${tool.endpoint ?? ""}`);
2798
+ if (tool.description) {
2799
+ console.log(` ${import_chalk11.default.dim(tool.description)}`);
2800
+ }
2801
+ if (tool.auth_required) {
2802
+ console.log(` ${import_chalk11.default.yellow("\u2691 requires auth")}`);
2803
+ }
2804
+ if (tool.rate_limit) {
2805
+ console.log(` ${import_chalk11.default.dim("rate limit:")} ${tool.rate_limit}`);
2806
+ }
2807
+ }
2808
+ }
2809
+ let score = 0;
2810
+ if (manifest) score += 30;
2811
+ if (mode === "direct") score += 20;
2812
+ if (tools.length > 0) score += 20;
2813
+ if (toolsWithSchema.length > 0) score += 15;
2814
+ if (hasAuth) score += 10;
2815
+ if (manifest.verticals?.length) score += 5;
2816
+ console.log();
2817
+ console.log(import_chalk11.default.dim("\u2501".repeat(52)));
2818
+ console.log(
2819
+ import_chalk11.default.bold("Agent Compatibility Score: ") + (score >= 80 ? import_chalk11.default.green : score >= 50 ? import_chalk11.default.yellow : import_chalk11.default.red)(String(score)) + import_chalk11.default.dim("/100")
2820
+ );
2821
+ if (score === 100) {
2822
+ console.log(import_chalk11.default.green.bold("\n\u{1F389} Fully agent-compatible!\n"));
2823
+ } else if (score >= 80) {
2824
+ console.log(import_chalk11.default.green("\n\u2713 Good agent compatibility.\n"));
2825
+ } else if (score >= 50) {
2826
+ console.log(import_chalk11.default.yellow("\n\u26A0 Partial agent compatibility. Add missing fields to improve.\n"));
2827
+ } else {
2828
+ console.log(import_chalk11.default.red("\n\u2717 Low agent compatibility. Follow the recommendations above.\n"));
2829
+ }
2830
+ }
2831
+ function check2(ok, msg) {
2832
+ console.log((ok ? import_chalk11.default.green(" \u2713") : import_chalk11.default.yellow(" \u26A0")) + " " + msg);
2833
+ }
2834
+ async function fetchWithTimeout(url, ms) {
2835
+ const controller = new AbortController();
2836
+ const timer = setTimeout(() => controller.abort(), ms);
2837
+ try {
2838
+ return await fetch(url, {
2839
+ signal: controller.signal,
2840
+ headers: { Accept: "application/json", "User-Agent": "OCPBot/1.0 (+https://opencommerceprotocol.org/bot)" }
2841
+ });
2842
+ } finally {
2843
+ clearTimeout(timer);
2844
+ }
2845
+ }
2846
+ function convertOCPToAgentManifest(ocp, baseUrl) {
2847
+ const merchant = ocp["merchant"];
2848
+ const interact = ocp["interact"];
2849
+ const bridge = ocp["bridge"];
2850
+ const toolNames = Array.isArray(interact?.["tools"]) ? interact["tools"] : ["search_products", "get_product"];
2851
+ const tools = toolNames.map((name) => ({
2852
+ name,
2853
+ endpoint: `/api/ocp/${name}`,
2854
+ method: "POST",
2855
+ description: `OCP tool: ${name}`
2856
+ }));
2857
+ const manifest = {
2858
+ version: "1.0",
2859
+ name: typeof merchant?.["name"] === "string" ? merchant["name"] : new URL(baseUrl).hostname,
2860
+ tools,
2861
+ verticals: ["commerce"],
2862
+ tool_format: "jsonschema"
2863
+ };
2864
+ if (typeof merchant?.["description"] === "string") {
2865
+ manifest.description = merchant["description"];
2866
+ }
2867
+ if (bridge) {
2868
+ manifest.bridge = {};
2869
+ if (typeof bridge["mcp"] === "string") manifest.bridge["mcp"] = bridge["mcp"];
2870
+ if (typeof bridge["a2a"] === "string") manifest.bridge["a2a"] = bridge["a2a"];
2871
+ if (typeof bridge["acp"] === "string") manifest.bridge["acp"] = bridge["acp"];
2872
+ }
2873
+ return manifest;
2874
+ }
2875
+ function toMCPTools(manifest) {
2876
+ return (manifest.tools ?? []).map((tool) => ({
2877
+ name: tool.name,
2878
+ description: tool.description,
2879
+ inputSchema: buildInputSchema(tool)
2880
+ }));
2881
+ }
2882
+ function toOpenAITools(manifest) {
2883
+ return (manifest.tools ?? []).map((tool) => ({
2884
+ type: "function",
2885
+ function: {
2886
+ name: tool.name,
2887
+ description: tool.description,
2888
+ parameters: buildInputSchema(tool)
2889
+ }
2890
+ }));
2891
+ }
2892
+ function buildInputSchema(tool) {
2893
+ if (tool.parameters && typeof tool.parameters === "object") {
2894
+ return tool.parameters;
2895
+ }
2896
+ if (tool.schema && typeof tool.schema === "object") {
2897
+ return tool.schema;
2898
+ }
2899
+ return { type: "object", properties: {} };
2900
+ }
2901
+
2902
+ // src/cli.ts
2903
+ var program = new import_commander.Command();
2904
+ program.name("ocp").description(import_chalk12.default.bold("Open Commerce Protocol") + " \u2014 Make any website shoppable by AI agents").version("1.0.0").addHelpText(
2905
+ "after",
2906
+ `
2907
+ ${import_chalk12.default.bold("Examples:")}
2908
+ ${import_chalk12.default.cyan("$ ocp init")} Interactive setup wizard
2909
+ ${import_chalk12.default.cyan("$ ocp crawl https://mystore.com")} Extract products from Schema.org JSON-LD
2910
+ ${import_chalk12.default.cyan("$ ocp validate https://mystore.com")} Validate a live OCP implementation
2911
+ ${import_chalk12.default.cyan("$ ocp generate --products data.csv")} Generate feed from product CSV
2912
+ ${import_chalk12.default.cyan("$ ocp robots")} Add OCP-Manifest to robots.txt
2913
+ ${import_chalk12.default.cyan("$ ocp llms")} Generate llms.txt Commerce section
2914
+ ${import_chalk12.default.cyan("$ ocp bridge --protocol mcp")} Generate MCP server bridge
2915
+ ${import_chalk12.default.cyan("$ ocp stats --log /var/log/nginx/access.log")} Analyze agent traffic in logs
2916
+ ${import_chalk12.default.cyan("$ ocp test-agent https://mystore.com")} Simulate an agent visiting your store
2917
+ ${import_chalk12.default.cyan("$ ocp preview https://mystore.com")} See what agents see in your store
2918
+ ${import_chalk12.default.cyan("$ ocp agent-discover https://mystore.com")} Discover agent tools at any URL
2919
+
2920
+ ${import_chalk12.default.dim("Documentation: https://opencommerceprotocol.org/docs")}
2921
+ `
2922
+ );
2923
+ program.command("init").description("Interactive wizard to generate OCP files for your store").option("-o, --output <dir>", "Output directory", ".").action(async (options) => {
2924
+ await runInit(options.output);
2925
+ });
2926
+ program.command("validate").description("Validate an OCP implementation (URL or local directory)").argument("<target>", "URL (https://mystore.com) or local directory path").action(async (target) => {
2927
+ await runValidate(target);
2928
+ });
2929
+ program.command("generate").description("Generate OCP files from existing product data (CSV, JSON, or JSONL)").option("-p, --products <file>", "Input product file (.csv, .json, or .jsonl)").option("-c, --config <file>", "OCP config file").option("-o, --output <dir>", "Output directory", ".").action(async (options) => {
2930
+ await runGenerate(options);
2931
+ });
2932
+ program.command("bridge").description("Generate bridge configuration for other agent protocols").option("--protocol <protocol>", "Target protocol (mcp, ucp, a2a)", "mcp").option("-o, --output <dir>", "Output directory", "./bridge").option("-m, --manifest <file>", "OCP manifest file", ".well-known/ocp.json").action(async (options) => {
2933
+ await runBridge(options);
2934
+ });
2935
+ program.command("crawl").description("Crawl a website, extract Schema.org Product JSON-LD, and generate OCP files").argument("<url>", "Base URL of the website to crawl").option("-o, --output <dir>", "Output directory", "./ocp-output").option("-c, --concurrency <n>", "Max concurrent requests", "5").option("--product-pattern <regex>", "URL pattern to identify product pages").option("--max-products <n>", "Maximum products to crawl").option("--delay <ms>", "Delay between requests in ms", "200").option("--user-agent <string>", "Custom User-Agent", "OCPBot/1.0 (+https://opencommerceprotocol.org/bot)").option("--include-variants", "Extract product variants", true).option("--generate-agent-notes", "Auto-generate agent_notes field", true).option("--dry-run", "List URLs that would be crawled without fetching", false).option("--verbose", "Print detailed extraction info per URL", false).option("--resume", "Resume a previously interrupted crawl", false).action(async (url, options) => {
2936
+ const crawlOptions = {
2937
+ output: options["output"],
2938
+ concurrency: parseInt(options["concurrency"], 10) || 5,
2939
+ delay: parseInt(options["delay"], 10) || 200,
2940
+ userAgent: options["userAgent"],
2941
+ includeVariants: options["includeVariants"] !== false,
2942
+ generateAgentNotes: options["generateAgentNotes"] !== false,
2943
+ dryRun: options["dryRun"] === true,
2944
+ verbose: options["verbose"] === true,
2945
+ resume: options["resume"] === true
2946
+ };
2947
+ if (options["productPattern"]) {
2948
+ crawlOptions.productPattern = options["productPattern"];
2949
+ }
2950
+ if (options["maxProducts"]) {
2951
+ crawlOptions.maxProducts = parseInt(options["maxProducts"], 10);
2952
+ }
2953
+ await runCrawl(url, crawlOptions);
2954
+ });
2955
+ program.command("robots").description("Add OCP-Manifest directive to robots.txt").argument("[path]", "Path to robots.txt file").option("--stdout", "Print modified content to stdout", false).option("--url <manifest-url>", "Custom manifest URL").action(async (filePath, options) => {
2956
+ await runRobots(filePath, options);
2957
+ });
2958
+ program.command("llms").description("Generate or patch llms.txt with OCP Commerce section").argument("[path]", "Path to llms.txt file").option("--stdout", "Print content to stdout", false).option("--full", "Also generate llms-full.txt with product descriptions", false).action(async (filePath, options) => {
2959
+ await runLlms(filePath, options);
2960
+ });
2961
+ program.command("stats").description("Analyze server access logs for OCP agent traffic patterns").argument("[url]", "Site URL (for contextual suggestions)").option("-l, --log <file>", "Path to server access log file").option("-f, --format <format>", "Log format: nginx, apache, cloudflare", "nginx").option("-d, --days <n>", "Number of days to analyze", "30").action(async (url, options) => {
2962
+ const statsOpts = {
2963
+ format: options.format ?? "nginx",
2964
+ days: parseInt(options.days ?? "30", 10)
2965
+ };
2966
+ if (options.log) {
2967
+ statsOpts.log = options.log;
2968
+ }
2969
+ await runStats(url, statsOpts);
2970
+ });
2971
+ program.command("test-agent").description("Simulate an AI agent visiting your store and output a pass/fail report").argument("<url>", "Base URL of the OCP-enabled store").option("-q, --query <query>", "Test search query", "product").option("-v, --verbose", "Show detailed output including sample agent_notes", false).action(async (url, options) => {
2972
+ const testOpts = {
2973
+ verbose: options.verbose ?? false
2974
+ };
2975
+ if (options.query) {
2976
+ testOpts.query = options.query;
2977
+ }
2978
+ await runTestAgent(url, testOpts);
2979
+ });
2980
+ program.command("preview").description("Render what an AI agent sees when visiting your store").argument("<url>", "Base URL of the OCP-enabled store").option("-p, --products <n>", "Number of sample products to show", "5").option("-v, --verbose", "Show full ocp.md and best/worst agent_notes", false).action(async (url, options) => {
2981
+ await runPreview(url, {
2982
+ products: parseInt(options.products ?? "5", 10),
2983
+ verbose: options.verbose ?? false
2984
+ });
2985
+ });
2986
+ program.command("agent-discover").description("Discover agent tools at a URL via /.well-known/agent.json (Mode A) or OCP fallback").argument("<url>", "URL to discover agent tools at (e.g. https://mystore.com)").option("-v, --verbose", "Show full tool list with descriptions", false).option("--format <format>", "Output format: text, json, mcp, openai", "text").addHelpText(
2987
+ "after",
2988
+ `
2989
+ ${import_chalk12.default.bold("Examples:")}
2990
+ ${import_chalk12.default.cyan("$ ocp agent-discover https://example.com")} Discover and validate agent tools
2991
+ ${import_chalk12.default.cyan("$ ocp agent-discover https://example.com --verbose")} Show full tool details
2992
+ ${import_chalk12.default.cyan("$ ocp agent-discover https://example.com --format mcp")} Export as MCP tool definitions
2993
+ ${import_chalk12.default.cyan("$ ocp agent-discover https://example.com --format openai")} Export as OpenAI tool definitions
2994
+ ${import_chalk12.default.cyan("$ ocp agent-discover https://example.com --format json")} Raw manifest JSON
2995
+ `
2996
+ ).action(async (url, options) => {
2997
+ const discoverOptions = {
2998
+ verbose: options.verbose ?? false
2999
+ };
3000
+ const fmt = options.format ?? "text";
3001
+ if (fmt !== void 0) discoverOptions.format = fmt;
3002
+ await runAgentDiscover(url, discoverOptions);
3003
+ });
3004
+ program.parseAsync(process.argv).catch((err) => {
3005
+ console.error(import_chalk12.default.red("\nError:"), err instanceof Error ? err.message : String(err));
3006
+ process.exit(1);
3007
+ });