@datadive-tools/mcp 0.16.1 → 0.17.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/lib.js CHANGED
@@ -139,7 +139,7 @@ function extractMessage(body) {
139
139
  }
140
140
 
141
141
  // package.json
142
- var version = "0.16.1";
142
+ var version = "0.17.0";
143
143
 
144
144
  // src/http/client.ts
145
145
  var PKG_VERSION = version;
@@ -316,7 +316,7 @@ var inputSchema2 = {
316
316
  var getNicheKeywordsTool = {
317
317
  name: "get_niche_keywords",
318
318
  title: "Get Master Keyword List for a Niche",
319
- description: 'Use this when the user asks about keywords, search terms, or search volume for a niche. Retrieves the master keyword list for the specified Niche \u2014 the relevant search terms monitored for ranking and performance metrics. For each keyword, returns search volume, relevancy (numeric score or "Outlier"), and competitor ASIN organic ranks (asinRanks: { ASIN -> rank | null }). Also returns the latestResearchDate.',
319
+ description: 'Use this when the user asks about keywords, search terms, or search volume for a niche. Retrieves the master keyword list for the specified Niche \u2014 the relevant search terms monitored for ranking and performance metrics. For each keyword, returns search volume, relevancy (numeric score or "Outlier"), and competitor ASIN organic ranks (asinRanks: { ASIN -> rank | null }), plus `origin`: `mkl` (found by the dive), `manual` (added by hand with `add_niche_keywords`), `outlier` / `residue` (moved in from those lists). Only `manual`, `outlier` and `residue` keywords can be taken out with `remove_niche_keywords`. Keywords hidden by the niche\'s keyword exclusions are not listed; see `list_keyword_exclusions`. Also returns the latestResearchDate.',
320
320
  inputSchema: inputSchema2,
321
321
  annotations: { readOnlyHint: true },
322
322
  handler: async (args, ctx) => {
@@ -327,16 +327,35 @@ var getNicheKeywordsTool = {
327
327
  }
328
328
  };
329
329
 
330
- // src/tools/get-niche-roots.ts
330
+ // src/tools/list-keyword-exclusions.ts
331
331
  import { z as z3 } from "zod";
332
332
  var inputSchema3 = {
333
333
  nicheId: z3.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
334
334
  };
335
+ var listKeywordExclusionsTool = {
336
+ name: "list_keyword_exclusions",
337
+ title: "List a Niche's Keyword Exclusions",
338
+ description: "Use this to see which keywords are hidden from a niche's Master Keyword List, e.g. when a keyword the user expects is missing from `get_niche_keywords`. Returns the same lists the DataDive web app shows, oldest first: `exact` \u2014 each entry hides the one keyword that matches it exactly, with that keyword's `searchVolume` (0 when the niche has no such keyword); `phrase` \u2014 each entry hides every keyword that contains the phrase, with `excludedKeywordsCount` and the total `searchVolume` of the keywords it hides (not counting ones already excluded exactly). Exclusions hide keywords wherever they come from: the dive, the Outliers and Residue lists, and hand-added ones. Change them with `add_keyword_exclusions` and `remove_keyword_exclusions`.",
339
+ inputSchema: inputSchema3,
340
+ annotations: { readOnlyHint: true },
341
+ handler: async (args, ctx) => {
342
+ return await httpGet(
343
+ { config: ctx.config, toolName: "list_keyword_exclusions" },
344
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keyword-exclusions`
345
+ );
346
+ }
347
+ };
348
+
349
+ // src/tools/get-niche-roots.ts
350
+ import { z as z4 } from "zod";
351
+ var inputSchema4 = {
352
+ nicheId: z4.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
353
+ };
335
354
  var getNicheRootsTool = {
336
355
  name: "get_niche_roots",
337
356
  title: "Get Keyword Roots for a Niche",
338
357
  description: "Use this when the user wants to find the highest-impact words across a niche's keywords, asks about 'keyword roots', or wants to prioritize terms for a listing. Retrieves the keyword lexical roots for the Niche \u2014 individual words or word-combinations extracted from the Master Keyword List (e.g. 'bluetooth headphones' yields roots 'bluetooth', 'headphones', 'bluetooth headphones'). Returns `roots` and `normalizedRoots` tables, each item with root, frequency (how many keywords contain it), broadSearchVolume (summed search volume of those keywords), and broadSearchVolumeRatio (0-1, relative to the top root), plus the per-keyword breakdowns and latestResearchDate.",
339
- inputSchema: inputSchema3,
358
+ inputSchema: inputSchema4,
340
359
  annotations: { readOnlyHint: true },
341
360
  handler: async (args, ctx) => {
342
361
  return await httpGet(
@@ -347,15 +366,15 @@ var getNicheRootsTool = {
347
366
  };
348
367
 
349
368
  // src/tools/get-niche-competitors.ts
350
- import { z as z4 } from "zod";
351
- var inputSchema4 = {
352
- nicheId: z4.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
369
+ import { z as z5 } from "zod";
370
+ var inputSchema5 = {
371
+ nicheId: z5.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
353
372
  };
354
373
  var getNicheCompetitorsTool = {
355
374
  name: "get_niche_competitors",
356
375
  title: "Get Competitors and Niche Statistics",
357
376
  description: "Use this for competitor ASINs, product titles, BSR, sales/revenue benchmarks, or niche opportunity scoring. Retrieves the list of Competitors within the specified Niche along with Niche statistics. For the Niche: keyword statistics, opportunity evaluation, benchmark median values, and overall competitor strength assessment. For each Competitor: ASIN, product title, BSR, category and full category tree, sales, revenue, ratings, reviews, price, number of variations, image URL, total Ranking Juice, and ranking data. BSR supports ranking Competitors by sales position within mentioned category. For the Ranking Juice breakdown by title, bullets and description, use `get_ranking_juice`.",
358
- inputSchema: inputSchema4,
377
+ inputSchema: inputSchema5,
359
378
  annotations: { readOnlyHint: true },
360
379
  handler: async (args, ctx) => {
361
380
  return await httpGet(
@@ -366,15 +385,15 @@ var getNicheCompetitorsTool = {
366
385
  };
367
386
 
368
387
  // src/tools/get-ranking-juice.ts
369
- import { z as z5 } from "zod";
370
- var inputSchema5 = {
371
- nicheId: z5.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
388
+ import { z as z6 } from "zod";
389
+ var inputSchema6 = {
390
+ nicheId: z6.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
372
391
  };
373
392
  var getRankingJuiceTool = {
374
393
  name: "get_ranking_juice",
375
394
  title: "Get Ranking Juices for a Niche",
376
395
  description: "Use this when the user asks 'how do I rank higher', 'what's my ranking juice', or wants listing-optimization guidance. Retrieves the Ranking Juices for each Competitor within the specified Niche. Ranking Juice is DataDive's proprietary metric identifying the key factors driving search-ranking success for each Competitor's listing. Returns currentListing and optimizedListing breakdowns (rankingJuice + per-property contributions: title, bullets, description) plus per-competitor listing scores and the latestResearchDate.",
377
- inputSchema: inputSchema5,
396
+ inputSchema: inputSchema6,
378
397
  annotations: { readOnlyHint: true },
379
398
  handler: async (args, ctx) => {
380
399
  return await httpGet(
@@ -385,7 +404,7 @@ var getRankingJuiceTool = {
385
404
  };
386
405
 
387
406
  // src/tools/list-rank-radars.ts
388
- import { z as z6 } from "zod";
407
+ import { z as z7 } from "zod";
389
408
 
390
409
  // src/types/api.ts
391
410
  var ISO_8601 = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
@@ -401,6 +420,10 @@ var SUPPORTED_MARKETPLACES = [
401
420
  "it",
402
421
  "co.jp"
403
422
  ];
423
+ var NICHE_KEYWORDS_MAX_PER_CALL = 100;
424
+ var NICHE_KEYWORD_MAX_LENGTH = 255;
425
+ var KEYWORD_EXCLUSIONS_MAX_PER_CALL = 100;
426
+ var KEYWORD_EXCLUSIONS_MAX_PER_NICHE = 1e3;
404
427
  var RANK_RADAR_STATES = ["ACTIVE", "PAUSED", "ARCHIVED"];
405
428
  var RANK_RADAR_STATUSES = [...RANK_RADAR_STATES, "ALL"];
406
429
  var PPC_CAMPAIGN_BASE_SORT_BY = [
@@ -446,14 +469,14 @@ var LISTING_COPY_STRATEGIES = ["cosmo", "ranking-juice", "nlp", "cosmo-rufus"];
446
469
 
447
470
  // src/tools/list-rank-radars.ts
448
471
  var LIST_RANK_RADARS_MAX_PAGE_SIZE = 50;
449
- var inputSchema6 = {
450
- currentPage: z6.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
451
- pageSize: z6.number().int().min(1).max(LIST_RANK_RADARS_MAX_PAGE_SIZE).optional().describe(`Items per page (max ${LIST_RANK_RADARS_MAX_PAGE_SIZE}). Defaults to 20.`),
452
- nicheId: z6.string().min(1).optional().describe("Filter Rank Radars by Niche identifier. Use after `list_niches`."),
453
- status: z6.enum(RANK_RADAR_STATUSES).optional().describe(
472
+ var inputSchema7 = {
473
+ currentPage: z7.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
474
+ pageSize: z7.number().int().min(1).max(LIST_RANK_RADARS_MAX_PAGE_SIZE).optional().describe(`Items per page (max ${LIST_RANK_RADARS_MAX_PAGE_SIZE}). Defaults to 20.`),
475
+ nicheId: z7.string().min(1).optional().describe("Filter Rank Radars by Niche identifier. Use after `list_niches`."),
476
+ status: z7.enum(RANK_RADAR_STATUSES).optional().describe(
454
477
  "Which Rank Radars to return. ACTIVE (the default) are tracking; PAUSED have been stopped but keep their history and can be resumed; ARCHIVED have been deleted; ALL means active and paused together. Archived Rank Radars are returned only by an explicit ARCHIVED."
455
478
  ),
456
- searchText: z6.string().optional().describe("Filter Rank Radars by ASIN or product title.")
479
+ searchText: z7.string().optional().describe("Filter Rank Radars by ASIN or product title.")
457
480
  };
458
481
  function minimize(item) {
459
482
  const { asin, ...rest } = item;
@@ -463,7 +486,7 @@ var listRankRadarsTool = {
463
486
  name: "list_rank_radars",
464
487
  title: "List Rank Radars",
465
488
  description: `Use this to find a \`rankRadarId\` before calling \`get_rank_radar_data\`. Filter by \`nicheId\` if the user has already identified a niche. Retrieves a paginated list of Rank Radars \u2014 keyword-rank trackers monitoring organic and sponsored positions for specific ASINs over time. Each item includes id, status (ACTIVE, PAUSED or ARCHIVED), asin, marketplace, keywordCount, title, imageUrl, and summary metrics: top10KW, top10SV, top50KW, top50SV. Supports pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev); a page holds at most ${LIST_RANK_RADARS_MAX_PAGE_SIZE} Rank Radars, so a large account needs several calls.`,
466
- inputSchema: inputSchema6,
489
+ inputSchema: inputSchema7,
467
490
  annotations: { readOnlyHint: true },
468
491
  handler: async (args, ctx) => {
469
492
  const page = await httpGet(
@@ -482,18 +505,18 @@ var listRankRadarsTool = {
482
505
  };
483
506
 
484
507
  // src/tools/rank-radar-keyword-query.ts
485
- import { z as z7 } from "zod";
508
+ import { z as z8 } from "zod";
486
509
  var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
487
510
  var RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE = 100;
488
511
  var RANK_RADAR_MAX_DATE_RANGE_DAYS = 90;
489
512
  var rankRadarKeywordQueryInputSchema = {
490
- rankRadarId: z7.string().uuid().describe("The Rank Radar UUID (from `list_rank_radars`)."),
491
- startDate: z7.string().regex(ISO_DATE, "startDate must be in yyyy-mm-dd format").describe("Start date of the range, yyyy-mm-dd (e.g. 2024-03-26)."),
492
- endDate: z7.string().regex(ISO_DATE, "endDate must be in yyyy-mm-dd format").describe(
513
+ rankRadarId: z8.string().uuid().describe("The Rank Radar UUID (from `list_rank_radars`)."),
514
+ startDate: z8.string().regex(ISO_DATE, "startDate must be in yyyy-mm-dd format").describe("Start date of the range, yyyy-mm-dd (e.g. 2024-03-26)."),
515
+ endDate: z8.string().regex(ISO_DATE, "endDate must be in yyyy-mm-dd format").describe(
493
516
  `End date of the range, yyyy-mm-dd (e.g. 2024-04-26). Must be on or after startDate, and at most ${RANK_RADAR_MAX_DATE_RANGE_DAYS} days after it.`
494
517
  ),
495
- currentPage: z7.number().int().min(1).optional().describe("Page of keywords, 1-indexed. Defaults to 1."),
496
- pageSize: z7.number().int().min(1).max(RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE).optional().describe(`Keywords per page (max ${RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE}). Defaults to 20.`)
518
+ currentPage: z8.number().int().min(1).optional().describe("Page of keywords, 1-indexed. Defaults to 1."),
519
+ pageSize: z8.number().int().min(1).max(RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE).optional().describe(`Keywords per page (max ${RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE}). Defaults to 20.`)
497
520
  };
498
521
  var RANK_RADAR_KEYWORD_PAGING_NOTE = `Results are paged by keyword: \`data\` holds one page, and the response carries currentPage, pageSize, total, lastPage, hasNext and hasPrev. \`total\` is the Rank Radar's active keyword count; to read every keyword, call again with \`currentPage\` + 1 while \`hasNext\` is true, using \`pageSize\` ${RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE} for large Rank Radars. Paused keywords are not included.`;
499
522
  function rankRadarPath(rankRadarId, suffix = "") {
@@ -509,7 +532,7 @@ function rankRadarKeywordQuery(args) {
509
532
  }
510
533
 
511
534
  // src/tools/get-rank-radar-data.ts
512
- var inputSchema7 = rankRadarKeywordQueryInputSchema;
535
+ var inputSchema8 = rankRadarKeywordQueryInputSchema;
513
536
  async function readAllKeywords(args, ctx) {
514
537
  const request = { config: ctx.config, toolName: "get_rank_radar_data" };
515
538
  const path = rankRadarPath(args.rankRadarId);
@@ -539,7 +562,7 @@ var getRankRadarDataTool = {
539
562
  name: "get_rank_radar_data",
540
563
  title: "Get Keyword Rankings for a Rank Radar",
541
564
  description: `Use this to analyze keyword ranking trends over time. Requires startDate and endDate (yyyy-mm-dd), at most ${RANK_RADAR_MAX_DATE_RANGE_DAYS} days apart. Retrieves historical keyword ranking data for the specified Rank Radar within the date range. Without \`currentPage\` and \`pageSize\` it returns every active keyword in one result. To read one page instead \u2014 faster for large Rank Radars \u2014 pass \`currentPage\` and/or \`pageSize\` (max ${RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE}) and continue with \`currentPage\` + 1 while \`hasNext\` is true. The response carries currentPage, pageSize, total, lastPage, hasNext and hasPrev; \`total\` is the Rank Radar's active keyword count. Paused keywords are not included. Each keyword has id, keyword, searchVolume, relevancy, ranks (per-day { date, organicRank, sponsoredRank, impressionRank }), and any highlight annotations. A rank of 101 means the product was not in the top 100 results that day. For PPC metrics use \`get_rank_radar_ppc_data\`; for Search Query Performance use \`get_rank_radar_sqp_data\`. Use after \`list_rank_radars\` to discover a \`rankRadarId\`.`,
542
- inputSchema: inputSchema7,
565
+ inputSchema: inputSchema8,
543
566
  annotations: { readOnlyHint: true },
544
567
  handler: async (args, ctx) => {
545
568
  if (args.currentPage === void 0 && args.pageSize === void 0) {
@@ -554,12 +577,12 @@ var getRankRadarDataTool = {
554
577
  };
555
578
 
556
579
  // src/tools/get-rank-radar-sqp-data.ts
557
- var inputSchema8 = rankRadarKeywordQueryInputSchema;
580
+ var inputSchema9 = rankRadarKeywordQueryInputSchema;
558
581
  var getRankRadarSqpDataTool = {
559
582
  name: "get_rank_radar_sqp_data",
560
583
  title: "Get Search Query Performance for a Rank Radar",
561
584
  description: `Use this when the user asks how shoppers search, click, add to cart and buy for the keywords of a Rank Radar \u2014 Amazon's Search Query Performance (SQP) data, from Brand Analytics. Requires startDate and endDate (yyyy-mm-dd), at most ${RANK_RADAR_MAX_DATE_RANGE_DAYS} days apart; 30 days or less keeps it fast. Each keyword's metrics are aggregated over the range: searchQueryVolume and searchQueryScore; impressions, clicks, cart adds and purchases, each as the total across all sellers (\`*TotalCount\`), this ASIN family's count (\`*AsinCount\`) and its share (\`*AsinShare\`); click, cart-add and purchase rates; and ctr/cvr for the market (\`*Total\`) and for this ASIN family (\`*Asin\`). \`numberOfDaysWithData\` says how many days of the range had SQP data; metrics are null when there is none, which is normal for low-volume keywords, recent dates (Amazon publishes SQP with a delay) and sellers without Brand Analytics. ${RANK_RADAR_KEYWORD_PAGING_NOTE} Each call counts toward API usage and is rate limited (about 60 requests/minute). Use after \`list_rank_radars\` to discover a \`rankRadarId\`.`,
562
- inputSchema: inputSchema8,
585
+ inputSchema: inputSchema9,
563
586
  annotations: { readOnlyHint: true },
564
587
  handler: async (args, ctx) => {
565
588
  return await httpGet(
@@ -571,10 +594,10 @@ var getRankRadarSqpDataTool = {
571
594
  };
572
595
 
573
596
  // src/tools/get-rank-radar-ppc-data.ts
574
- import { z as z8 } from "zod";
575
- var inputSchema9 = {
597
+ import { z as z9 } from "zod";
598
+ var inputSchema10 = {
576
599
  ...rankRadarKeywordQueryInputSchema,
577
- includeCampaigns: z8.boolean().optional().describe(
600
+ includeCampaigns: z9.boolean().optional().describe(
578
601
  "When true, each keyword also carries `campaigns`: the per-campaign/ad-group breakdown (campaign and ad group name, match type, targeting, spend, sales, clicks, impressions, orders, CPC, CTR, CVR, ACOS). Defaults to false; leave it off unless the user asks which campaigns drive a keyword."
579
602
  )
580
603
  };
@@ -582,7 +605,7 @@ var getRankRadarPpcDataTool = {
582
605
  name: "get_rank_radar_ppc_data",
583
606
  title: "Get PPC Data for a Rank Radar",
584
607
  description: `Use this when the user asks about advertising (Sponsored Products) performance for the keywords of a Rank Radar. Requires startDate and endDate (yyyy-mm-dd), at most ${RANK_RADAR_MAX_DATE_RANGE_DAYS} days apart; 30 days or less keeps it fast. Each keyword's metrics are aggregated over the range: sponsoredRank (median; 101 means not in the top 100 sponsored results), impressionRank and impressionRankShare; how many exact, phrase, broad and auto campaigns target it; organicSales, ppcSales, ppcSpend, costPerClicks, clickThroughRate, conversionRate, totalClicks, totalImpressions, totalOrders and acos. Metrics are null or 0 when the seller runs no ads on a keyword. Set \`includeCampaigns\` for the per-campaign breakdown. ${RANK_RADAR_KEYWORD_PAGING_NOTE} Each call counts toward API usage and is rate limited (about 60 requests/minute). For the seller's campaigns as a whole, use \`list_ppc_campaigns\`. Use after \`list_rank_radars\` to discover a \`rankRadarId\`.`,
585
- inputSchema: inputSchema9,
608
+ inputSchema: inputSchema10,
586
609
  annotations: { readOnlyHint: true },
587
610
  handler: async (args, ctx) => {
588
611
  return await httpGet(
@@ -594,7 +617,7 @@ var getRankRadarPpcDataTool = {
594
617
  };
595
618
 
596
619
  // src/tools/create-rank-radar.ts
597
- import { z as z9 } from "zod";
620
+ import { z as z10 } from "zod";
598
621
 
599
622
  // src/tools/confirm.ts
600
623
  function needsConfirmation(confirm, ctx) {
@@ -615,11 +638,11 @@ function requireConfirmation(confirm, ctx, costNote) {
615
638
 
616
639
  // src/tools/create-rank-radar.ts
617
640
  var PATH = "/v1/niches/rank-radars";
618
- var inputSchema10 = {
619
- asin: z9.string().min(1).describe("The ASIN to track keyword rankings for."),
620
- numberOfKeywords: z9.number().int().min(1).describe("How many keywords to track (minimum 1). More keywords = broader tracking but more Search Term tokens."),
621
- nicheId: z9.string().min(1).describe("The niche this Rank Radar belongs to. Get one from `list_niches`."),
622
- confirm: z9.boolean().optional().describe(
641
+ var inputSchema11 = {
642
+ asin: z10.string().min(1).describe("The ASIN to track keyword rankings for."),
643
+ numberOfKeywords: z10.number().int().min(1).describe("How many keywords to track (minimum 1). More keywords = broader tracking but more Search Term tokens."),
644
+ nicheId: z10.string().min(1).describe("The niche this Rank Radar belongs to. Get one from `list_niches`."),
645
+ confirm: z10.boolean().optional().describe(
623
646
  "Must be true to proceed \u2014 creating a Rank Radar spends Search Term tokens. Confirm the cost with the user first."
624
647
  )
625
648
  };
@@ -627,7 +650,7 @@ var createRankRadarTool = {
627
650
  name: "create_rank_radar",
628
651
  title: "Create a Rank Radar",
629
652
  description: "Use this to start tracking organic and sponsored keyword rankings for an ASIN within a niche. \u26A0\uFE0F Spends Search Term tokens (cost scales with `numberOfKeywords`) and cannot be undone \u2014 set `confirm: true` only after the user approves the cost. Not safe to retry: each call spends tokens again and creates a separate Rank Radar \u2014 if a call errors or times out, check `list_rank_radars` instead of re-calling. The first call previews the creation and may return `warnings` alongside the cost note \u2014 for example that this product family is already tracked by another Rank Radar. Show every warning `message` to the user verbatim, before asking them to confirm; do not summarise it or decide on their behalf. A warning never blocks creation. Returns the new `rankRadarId`; read its data later with `get_rank_radar_data`. Requires `asin`, `numberOfKeywords` (min 1), and a `nicheId` from `list_niches`.",
630
- inputSchema: inputSchema10,
653
+ inputSchema: inputSchema11,
631
654
  // destructiveHint is technically "additive" here (this creates a new Rank Radar, it
632
655
  // doesn't destroy data), but we flag it destructive so clients prompt before the
633
656
  // irreversible token spend. The confirm gate enforces this server-side regardless.
@@ -652,20 +675,20 @@ var createRankRadarTool = {
652
675
  };
653
676
 
654
677
  // src/tools/create-niche-dive.ts
655
- import { z as z10 } from "zod";
656
- var inputSchema11 = {
657
- marketplace: z10.enum(SUPPORTED_MARKETPLACES).describe(
678
+ import { z as z11 } from "zod";
679
+ var inputSchema12 = {
680
+ marketplace: z11.enum(SUPPORTED_MARKETPLACES).describe(
658
681
  "Amazon marketplace of the seed ASIN. Note these are full domain suffixes: com, ca, co.uk, com.mx, in, fr, de, es, it, co.jp (e.g. UK is `co.uk`, not `uk`)."
659
682
  ),
660
- asin: z10.string().min(1).describe("The seed ASIN to start the dive from (e.g. B08N5WRWNW). Competitors are built around it."),
661
- numberOfCompetitors: z10.number().int().min(2).describe("How many competitors to analyze (minimum 2). More competitors = deeper niche but more dive tokens."),
662
- confirm: z10.boolean().optional().describe("Must be true to proceed \u2014 creating a dive spends dive tokens. Confirm the cost with the user first.")
683
+ asin: z11.string().min(1).describe("The seed ASIN to start the dive from (e.g. B08N5WRWNW). Competitors are built around it."),
684
+ numberOfCompetitors: z11.number().int().min(2).describe("How many competitors to analyze (minimum 2). More competitors = deeper niche but more dive tokens."),
685
+ confirm: z11.boolean().optional().describe("Must be true to proceed \u2014 creating a dive spends dive tokens. Confirm the cost with the user first.")
663
686
  };
664
687
  var createNicheDiveTool = {
665
688
  name: "create_niche_dive",
666
689
  title: "Create a Niche Dive",
667
690
  description: "Use this to start new niche research from a seed ASIN. \u26A0\uFE0F Spends dive tokens (cost scales with `numberOfCompetitors`) and cannot be undone \u2014 set `confirm: true` only after the user approves the cost. Not safe to retry: each call spends tokens again and starts a separate dive \u2014 if a call errors or times out, poll `get_dive_status` (or check `list_niches`) instead of re-calling. The dive runs asynchronously: this returns immediately with a `diveId` and an `estimatedCompletionDate`. Poll `get_dive_status` with that `diveId` until it reports `success` (which carries the new `nicheId` for use with `list_niches`, `get_niche_keywords`, etc.). Requires `marketplace`, `asin`, and `numberOfCompetitors` (min 2).",
668
- inputSchema: inputSchema11,
691
+ inputSchema: inputSchema12,
669
692
  // destructiveHint is technically "additive" here (a dive creates a new niche, it
670
693
  // doesn't destroy data), but we flag it destructive so clients prompt before the
671
694
  // irreversible token spend. The confirm gate enforces this server-side regardless.
@@ -690,20 +713,20 @@ var createNicheDiveTool = {
690
713
  };
691
714
 
692
715
  // src/tools/create-niche-dive-from-competitors-list.ts
693
- import { z as z11 } from "zod";
716
+ import { z as z12 } from "zod";
694
717
  var DIVE_FROM_COMPETITORS_MIN_ASINS = 2;
695
718
  var DIVE_FROM_COMPETITORS_MAX_ASINS = 200;
696
- var normalizedAsins = z11.array(z11.string().trim().min(1).toUpperCase()).min(DIVE_FROM_COMPETITORS_MIN_ASINS).max(DIVE_FROM_COMPETITORS_MAX_ASINS).refine((asins) => new Set(asins).size === asins.length, {
719
+ var normalizedAsins = z12.array(z12.string().trim().min(1).toUpperCase()).min(DIVE_FROM_COMPETITORS_MIN_ASINS).max(DIVE_FROM_COMPETITORS_MAX_ASINS).refine((asins) => new Set(asins).size === asins.length, {
697
720
  message: "asins must not contain the same ASIN twice"
698
721
  });
699
- var inputSchema12 = {
700
- marketplace: z11.enum(SUPPORTED_MARKETPLACES).describe(
722
+ var inputSchema13 = {
723
+ marketplace: z12.enum(SUPPORTED_MARKETPLACES).describe(
701
724
  "Amazon marketplace where the ASINs are listed. Full domain suffixes: com, ca, co.uk, com.mx, in, fr, de, es, it, co.jp (e.g. UK is `co.uk`, not `uk`)."
702
725
  ),
703
726
  asins: normalizedAsins.describe(
704
727
  `The exact competitor ASINs to build the niche from, ${DIVE_FROM_COMPETITORS_MIN_ASINS} to ${DIVE_FROM_COMPETITORS_MAX_ASINS}, each once. No other competitors are added.`
705
728
  ),
706
- confirm: z11.boolean().optional().describe(
729
+ confirm: z12.boolean().optional().describe(
707
730
  "Must be true to proceed \u2014 this dive spends dive tokens for every ASIN. Confirm the cost with the user first."
708
731
  )
709
732
  };
@@ -711,7 +734,7 @@ var createNicheDiveFromCompetitorsListTool = {
711
734
  name: "create_niche_dive_from_competitors_list",
712
735
  title: "Create a Niche Dive from a Competitor List",
713
736
  description: `Use this when the user already knows which products belong in the niche \u2014 e.g. they list competitor ASINs, or picked them from search results \u2014 and wants niche research on exactly those. Unlike \`create_niche_dive\`, which discovers competitors around one seed ASIN, this builds the niche from the given ASINs only (${DIVE_FROM_COMPETITORS_MIN_ASINS}-${DIVE_FROM_COMPETITORS_MAX_ASINS}). \u26A0\uFE0F Spends dive tokens for every ASIN and cannot be undone \u2014 set \`confirm: true\` only after the user approves the cost. Not safe to retry: each call spends tokens again and starts a separate dive \u2014 if a call errors or times out, poll \`get_dive_status\` (or check \`list_niches\`) instead of re-calling. Fails without spending tokens when the ASIN count is over the plan's limit, when the account is at its niche limit, or when Amazon product data cannot be found for enough of the ASINs. The dive runs asynchronously: this returns a \`diveId\` and an \`estimatedCompletionDate\`; poll \`get_dive_status\` with the \`diveId\` until it reports \`success\`, which carries the new \`nicheId\`.`,
714
- inputSchema: inputSchema12,
737
+ inputSchema: inputSchema13,
715
738
  // Additive rather than destructive, but flagged destructive so clients prompt before the
716
739
  // irreversible token spend — same reasoning as create_niche_dive. The confirm gate enforces it.
717
740
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
@@ -731,23 +754,23 @@ var createNicheDiveFromCompetitorsListTool = {
731
754
  };
732
755
 
733
756
  // src/tools/redive-niche.ts
734
- import { z as z12 } from "zod";
735
- var inputSchema13 = {
736
- nicheId: z12.string().min(1).describe("The niche to re-dive. Get one from `list_niches`."),
737
- mode: z12.enum(["same_competitors", "discover"]).describe(
757
+ import { z as z13 } from "zod";
758
+ var inputSchema14 = {
759
+ nicheId: z13.string().min(1).describe("The niche to re-dive. Get one from `list_niches`."),
760
+ mode: z13.enum(["same_competitors", "discover"]).describe(
738
761
  "How to pick the competitors for the refreshed dive. `same_competitors` re-runs the niche's current competitor set and takes no other argument \u2014 use it to refresh stale data. `discover` searches for a fresh competitor set, which is what you want when the niche has changed."
739
762
  ),
740
- numberOfCompetitors: z12.number().int().min(2).optional().describe(
763
+ numberOfCompetitors: z13.number().int().min(2).optional().describe(
741
764
  "Total ASINs the refreshed dive should contain, including `lockedAsins`. Required in `discover` mode and rejected in `same_competitors` mode. Dive tokens are spent per ASIN."
742
765
  ),
743
- heroAsin: z12.string().min(1).optional().describe(
766
+ heroAsin: z13.string().min(1).optional().describe(
744
767
  "`discover` mode only. Seed product the competitor discovery starts from. Defaults to the niche's highest-selling competitor, preferring one the user's own connected seller account owns."
745
768
  ),
746
- lockedAsins: z12.array(z12.string().min(1)).optional().describe(
769
+ lockedAsins: z13.array(z13.string().min(1)).optional().describe(
747
770
  "`discover` mode only. ASINs that must stay in the competitor set regardless of what discovery finds. Cannot exceed `numberOfCompetitors`, and cannot overlap `excludedAsins`."
748
771
  ),
749
- excludedAsins: z12.array(z12.string().min(1)).optional().describe("`discover` mode only. ASINs discovery must never select."),
750
- confirm: z12.boolean().optional().describe(
772
+ excludedAsins: z13.array(z13.string().min(1)).optional().describe("`discover` mode only. ASINs discovery must never select."),
773
+ confirm: z13.boolean().optional().describe(
751
774
  "Must be true to proceed \u2014 a re-dive spends dive tokens. Confirm the cost with the user first."
752
775
  )
753
776
  };
@@ -755,7 +778,7 @@ var rediveNicheTool = {
755
778
  name: "redive_niche",
756
779
  title: "Re-dive an Existing Niche",
757
780
  description: "Use this to refresh an existing niche's research with current Amazon data, instead of creating a new niche with `create_niche_dive`. \u26A0\uFE0F Spends dive tokens (one batch per ASIN dived) and cannot be undone \u2014 set `confirm: true` only after the user approves the cost. Not safe to retry: each call spends tokens again and starts a separate re-dive \u2014 if a call errors or times out, poll `get_dive_status` instead of re-calling. Two modes: `same_competitors` re-dives the niche's current competitor set (no other argument needed) and `discover` finds a fresh set, sized by `numberOfCompetitors` and steerable with `heroAsin` / `lockedAsins` / `excludedAsins`. Runs asynchronously: returns a `diveId` and an `estimatedCompletionDate` \u2014 poll `get_dive_status` with that `diveId` until it reports `success`. The niche keeps its `nicheId`, so existing rank radars and reports follow the refreshed data.",
758
- inputSchema: inputSchema13,
781
+ inputSchema: inputSchema14,
759
782
  // Same reasoning as create_niche_dive: a re-dive is additive rather than destructive, but the
760
783
  // irreversible token spend is what clients need to prompt on. The confirm gate enforces it regardless.
761
784
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
@@ -788,17 +811,17 @@ var rediveNicheTool = {
788
811
  };
789
812
 
790
813
  // src/tools/get-dive-status.ts
791
- import { z as z13 } from "zod";
792
- var inputSchema14 = {
793
- diveId: z13.string().min(1).describe(
794
- "The dive identifier returned by `create_niche_dive`, `create_niche_dive_from_competitors_list` or `redive_niche`."
814
+ import { z as z14 } from "zod";
815
+ var inputSchema15 = {
816
+ diveId: z14.string().min(1).describe(
817
+ "The dive identifier returned by `create_niche_dive`, `create_niche_dive_from_competitors_list` or `redive_niche`, or the `refresh.diveId` returned by `add_niche_keywords`."
795
818
  )
796
819
  };
797
820
  var getDiveStatusTool = {
798
821
  name: "get_dive_status",
799
822
  title: "Get Niche Dive Status",
800
- description: "Use this to poll a niche dive started with `create_niche_dive`, `create_niche_dive_from_competitors_list` or `redive_niche` until it finishes. Returns one of three shapes keyed by `status`: `in_progress` (with `estimatedCompletionDate`), `success` (with the `nicheId` plus `tokensUsed`/`tokensLeft`), or `error` (with an `error` message). On `success`, use the `nicheId` with `list_niches`, `get_niche_keywords`, `get_niche_competitors`, etc. (for a re-dive it is the same niche you passed in, now carrying refreshed data). Re-poll periodically \u2014 dives can take minutes; the `estimatedCompletionDate` hints at when to check.",
801
- inputSchema: inputSchema14,
823
+ description: "Use this to poll a niche dive started with `create_niche_dive`, `create_niche_dive_from_competitors_list` or `redive_niche`, or the re-dive `add_niche_keywords` started on a stale niche, until it finishes. Returns one of three shapes keyed by `status`: `in_progress` (with `estimatedCompletionDate`), `success` (with the `nicheId` plus `tokensUsed`/`tokensLeft`), or `error` (with an `error` message). On `success`, use the `nicheId` with `list_niches`, `get_niche_keywords`, `get_niche_competitors`, etc. (for a re-dive it is the same niche you passed in, now carrying refreshed data). Re-poll periodically \u2014 dives can take minutes; the `estimatedCompletionDate` hints at when to check.",
824
+ inputSchema: inputSchema15,
802
825
  annotations: { readOnlyHint: true },
803
826
  handler: async (args, ctx) => {
804
827
  return await httpGet(
@@ -808,11 +831,166 @@ var getDiveStatusTool = {
808
831
  }
809
832
  };
810
833
 
834
+ // src/tools/add-niche-keywords.ts
835
+ import { z as z16 } from "zod";
836
+
837
+ // src/tools/niche-keyword-list.ts
838
+ import { z as z15 } from "zod";
839
+ var nicheKeyword = () => z15.string().trim().toLowerCase().min(1).max(NICHE_KEYWORD_MAX_LENGTH);
840
+ function uniqueKeywords(schema, field) {
841
+ return schema.refine((keywords) => new Set(keywords).size === keywords.length, {
842
+ message: `${field} must not contain the same keyword twice (compared lower-cased and trimmed)`
843
+ });
844
+ }
845
+ var nicheKeywordList = uniqueKeywords(
846
+ z15.array(nicheKeyword()).min(1).max(NICHE_KEYWORDS_MAX_PER_CALL),
847
+ "keywords"
848
+ );
849
+
850
+ // src/tools/add-niche-keywords.ts
851
+ var inputSchema16 = {
852
+ nicheId: z16.string().min(1).describe("The niche whose Master Keyword List to add to. Get one from `list_niches`."),
853
+ keywords: nicheKeywordList.describe(
854
+ `The keywords to add, 1\u2013${NICHE_KEYWORDS_MAX_PER_CALL} per call, at most 255 characters each. They are lower-cased and trimmed, and must be unique after that. Put every keyword in one call: this tool allows only 2 calls per minute per niche.`
855
+ ),
856
+ confirm: z16.boolean().optional().describe(
857
+ "Leave unset on the first call. Set to true only after the user approved the Dive token cost from a `confirmation_required` response \u2014 it lets the add re-dive a stale niche."
858
+ ),
859
+ maxTokensUsed: z16.number().int().min(0).optional().describe(
860
+ "With `confirm: true`: the `tokenCost` the user approved. The call then fails with nothing saved or charged rather than spend more. Ignored without `confirm`, where the tool never spends anything."
861
+ )
862
+ };
863
+ function isMaxTokensExceededBody(body) {
864
+ if (!body || typeof body !== "object") return false;
865
+ const b = body;
866
+ return b.error === "MAX_TOKENS_EXCEEDED" && typeof b.tokensUsed === "number";
867
+ }
868
+ var addNicheKeywordsTool = {
869
+ name: "add_niche_keywords",
870
+ title: "Add Keywords to a Niche's Master Keyword List",
871
+ description: "Use this to add keywords to a niche's Master Keyword List (MKL): the user's own search terms, or keywords from the niche's Outliers or Residue lists. Free while the niche is fresh (dived in the last 24 hours, or a dive is running): the first call adds the keywords at once and they are readable with `get_niche_keywords`. On a stale niche, any keyword new to the niche makes the add re-dive the whole niche with its current competitors, which spends one Dive token per competitor. The first call then saves and spends nothing and returns `confirmation_required` with `tokenCost`; after the user approves, call again with `confirm: true` and `maxTokensUsed` set to that `tokenCost`. Keywords moved in from Outliers or Residue never cost anything. Returns `originBreakdown` (`alreadyInMkl`, `takenFromOutliers`, `takenFromResidue`, `addedManually`) and `tokensUsed`. When `refresh` is present a re-dive is running: poll `get_dive_status` with `refresh.diveId`; the `addedManually` keywords show in the MKL once it succeeds. A confirmed call is not safe to retry: if it errors or times out, poll `get_dive_status` or re-read `get_niche_keywords` instead of re-calling. Limited to 2 calls per minute per niche, so batch keywords into one call. Undo with `remove_niche_keywords`. A keyword hidden by the niche's keyword exclusions stays hidden after an add: check `list_keyword_exclusions` and remove the exclusion with `remove_keyword_exclusions` first.",
872
+ inputSchema: inputSchema16,
873
+ // Like redive_niche: additive, but it can spend Dive tokens irreversibly, which is what clients
874
+ // need to prompt on. The confirm gate covers the paid path regardless.
875
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
876
+ handler: async (args, ctx) => {
877
+ const request = { config: ctx.config, toolName: "add_niche_keywords" };
878
+ const path = `/v1/niches/${encodeURIComponent(args.nicheId)}/keywords`;
879
+ if (needsConfirmation(args.confirm, ctx)) {
880
+ try {
881
+ return await httpPost(request, path, {
882
+ keywords: args.keywords,
883
+ maxTokensUsed: 0
884
+ });
885
+ } catch (err) {
886
+ if (err instanceof ApiError && isMaxTokensExceededBody(err.body)) {
887
+ const tokenCost = err.body.tokensUsed;
888
+ return {
889
+ ...confirmationRequired(
890
+ `This niche was last dived more than 24 hours ago, so adding keywords that are new to it re-dives the whole niche with its current competitors: up to ${tokenCost} Dive tokens, one per competitor (less if some can no longer be found). Nothing was added or charged yet.`
891
+ ),
892
+ tokenCost
893
+ };
894
+ }
895
+ throw err;
896
+ }
897
+ }
898
+ return await httpPost(request, path, {
899
+ keywords: args.keywords,
900
+ maxTokensUsed: args.maxTokensUsed
901
+ });
902
+ }
903
+ };
904
+
905
+ // src/tools/remove-niche-keywords.ts
906
+ import { z as z17 } from "zod";
907
+ var inputSchema17 = {
908
+ nicheId: z17.string().min(1).describe("The niche whose Master Keyword List to remove from. Get one from `list_niches`."),
909
+ keywords: nicheKeywordList.describe(
910
+ `The keywords to take out, 1\u2013${NICHE_KEYWORDS_MAX_PER_CALL} per call, at most 255 characters each. They are lower-cased and trimmed, and must be unique after that. Put every keyword in one call: this tool allows only 2 calls per minute per niche.`
911
+ )
912
+ };
913
+ var removeNicheKeywordsTool = {
914
+ name: "remove_niche_keywords",
915
+ title: "Remove Keywords from a Niche's Master Keyword List",
916
+ description: "Use this to undo `add_niche_keywords`: take keywords back out of a niche's Master Keyword List (MKL). Only keywords that were added to the MKL can be removed \u2014 `get_niche_keywords` shows each keyword's `origin`. What happens depends on that origin, and the response reports it per keyword: `removedManual` \u2014 added by hand (`manual`), deleted from the niche; `sentBackToOutliers` / `sentBackToResidue` \u2014 moved in from those lists (`outlier` / `residue`), listed there again with their data kept; `notRemovable` \u2014 found by the dive itself (`mkl`) or not in the niche, left unchanged. To hide a keyword the dive found, use `add_keyword_exclusions` instead. Free, and safe to repeat: a keyword already removed comes back under `notRemovable`. Adding a hand-added keyword back later re-dives the niche and costs Dive tokens if the niche is stale by then. Limited to 2 calls per minute per niche.",
917
+ inputSchema: inputSchema17,
918
+ // Deletes hand-added keywords, and putting them back can cost Dive tokens on a stale niche.
919
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
920
+ handler: async (args, ctx) => {
921
+ return await httpPost(
922
+ { config: ctx.config, toolName: "remove_niche_keywords" },
923
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keywords/remove`,
924
+ { keywords: args.keywords }
925
+ );
926
+ }
927
+ };
928
+
929
+ // src/tools/keyword-exclusions-input.ts
930
+ import { z as z18 } from "zod";
931
+ var entries = (field) => uniqueKeywords(z18.array(nicheKeyword()).max(KEYWORD_EXCLUSIONS_MAX_PER_CALL), field).optional();
932
+ var keywordExclusionsInputSchema = (verb) => ({
933
+ nicheId: z18.string().min(1).describe("The niche whose keyword exclusions to change. Get one from `list_niches`."),
934
+ exact: entries("exact").describe(
935
+ `Exact-match entries to ${verb}: each one hides the single keyword that matches it exactly. At most 255 characters each; lower-cased and trimmed, and must be unique after that.`
936
+ ),
937
+ phrase: entries("phrase").describe(
938
+ `Phrase-match entries to ${verb}: each one hides every keyword that contains the phrase (e.g. "puppy" hides "puppy hat" and "hat for puppy"). At most 255 characters each; lower-cased and trimmed, and must be unique after that.`
939
+ )
940
+ });
941
+ function assertExclusionCount(exact, phrase) {
942
+ const total = (exact?.length ?? 0) + (phrase?.length ?? 0);
943
+ if (total === 0 || total > KEYWORD_EXCLUSIONS_MAX_PER_CALL) {
944
+ throw new ApiError(
945
+ "bad_request",
946
+ 400,
947
+ `Send 1 to ${KEYWORD_EXCLUSIONS_MAX_PER_CALL} entries in total across \`exact\` and \`phrase\` (got ${total}). Split a longer list over several calls.`
948
+ );
949
+ }
950
+ }
951
+
952
+ // src/tools/add-keyword-exclusions.ts
953
+ var inputSchema18 = keywordExclusionsInputSchema("exclude");
954
+ var addKeywordExclusionsTool = {
955
+ name: "add_keyword_exclusions",
956
+ title: "Exclude Keywords from a Niche",
957
+ description: `Use this to hide irrelevant keywords from a niche's Master Keyword List \u2014 e.g. other brands, wrong product types. \`exact\` entries hide the one keyword that matches exactly; \`phrase\` entries hide every keyword containing the phrase, so prefer \`exact\` unless the user wants a whole family of keywords gone. The excluded keywords drop out of \`get_niche_keywords\` at once, wherever they come from: the dive, Outliers, Residue, or ones added with \`add_niche_keywords\`. Nothing is deleted: \`remove_keyword_exclusions\` brings them back. Only the exclusions change \u2014 every other niche setting is kept. Send 1\u2013${KEYWORD_EXCLUSIONS_MAX_PER_CALL} entries in total across \`exact\` and \`phrase\`; a niche can hold at most ${KEYWORD_EXCLUSIONS_MAX_PER_NICHE} exclusions. Free, no tokens, and safe to repeat: entries already excluded come back under \`unchanged\`. Returns \`added\`, \`unchanged\` and the full \`exact\` / \`phrase\` lists after the change. Limited to 30 calls per minute per niche, so batch entries into one call.`,
958
+ inputSchema: inputSchema18,
959
+ // Hides data from every MKL-based view until undone; reversible, but clients should prompt on it.
960
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
961
+ handler: async (args, ctx) => {
962
+ assertExclusionCount(args.exact, args.phrase);
963
+ return await httpPost(
964
+ { config: ctx.config, toolName: "add_keyword_exclusions" },
965
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keyword-exclusions`,
966
+ { exact: args.exact, phrase: args.phrase }
967
+ );
968
+ }
969
+ };
970
+
971
+ // src/tools/remove-keyword-exclusions.ts
972
+ var inputSchema19 = keywordExclusionsInputSchema("stop excluding");
973
+ var removeKeywordExclusionsTool = {
974
+ name: "remove_keyword_exclusions",
975
+ title: "Remove Keyword Exclusions from a Niche",
976
+ description: `Use this to undo \`add_keyword_exclusions\`, so the keywords an exclusion hid show in the niche's Master Keyword List (\`get_niche_keywords\`) again. Pass the entries exactly as \`list_keyword_exclusions\` shows them, \`exact\` and \`phrase\` separately: removing a phrase does not touch an exact entry for the same text, and a keyword also hidden by another remaining exclusion stays hidden. Only the exclusions change \u2014 every other niche setting is kept. Send 1\u2013${KEYWORD_EXCLUSIONS_MAX_PER_CALL} entries in total across \`exact\` and \`phrase\`. Free, and safe to repeat: entries that are not excluded come back under \`unchanged\`. Returns \`removed\`, \`unchanged\` and the full \`exact\` / \`phrase\` lists after the change. Limited to 30 calls per minute per niche.`,
977
+ inputSchema: inputSchema19,
978
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
979
+ handler: async (args, ctx) => {
980
+ assertExclusionCount(args.exact, args.phrase);
981
+ return await httpPost(
982
+ { config: ctx.config, toolName: "remove_keyword_exclusions" },
983
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keyword-exclusions/remove`,
984
+ { exact: args.exact, phrase: args.phrase }
985
+ );
986
+ }
987
+ };
988
+
811
989
  // src/tools/delete-niche.ts
812
- import { z as z14 } from "zod";
813
- var inputSchema15 = {
814
- nicheId: z14.string().min(1).describe("The niche to delete. Get one from `list_niches`."),
815
- confirm: z14.boolean().optional().describe(
990
+ import { z as z19 } from "zod";
991
+ var inputSchema20 = {
992
+ nicheId: z19.string().min(1).describe("The niche to delete. Get one from `list_niches`."),
993
+ confirm: z19.boolean().optional().describe(
816
994
  "Must be true to proceed \u2014 deleting a niche destroys its keywords, competitors and dive history permanently. Confirm with the user first."
817
995
  )
818
996
  };
@@ -820,7 +998,7 @@ var deleteNicheTool = {
820
998
  name: "delete_niche",
821
999
  title: "Delete a Niche",
822
1000
  description: "Use this to permanently remove a niche the user no longer needs. \u26A0\uFE0F Deletes the niche and everything attached to it \u2014 keywords, competitors, all dives and diveboxes \u2014 and cannot be undone; the dive tokens already spent on it are NOT refunded. Set `confirm: true` only after the user approves. A niche used by any Rank Radar cannot be deleted: the call fails with a conflict, so delete or archive those rank radars first (`list_rank_radars` with the `nicheId` filter shows them).",
823
- inputSchema: inputSchema15,
1001
+ inputSchema: inputSchema20,
824
1002
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
825
1003
  handler: async (args, ctx) => {
826
1004
  const pending = requireConfirmation(
@@ -838,15 +1016,15 @@ var deleteNicheTool = {
838
1016
  };
839
1017
 
840
1018
  // src/tools/pause-rank-radar.ts
841
- import { z as z15 } from "zod";
842
- var inputSchema16 = {
843
- rankRadarId: z15.string().uuid().describe("The Rank Radar UUID to pause (from `list_rank_radars`).")
1019
+ import { z as z20 } from "zod";
1020
+ var inputSchema21 = {
1021
+ rankRadarId: z20.string().uuid().describe("The Rank Radar UUID to pause (from `list_rank_radars`).")
844
1022
  };
845
1023
  var pauseRankRadarTool = {
846
1024
  name: "pause_rank_radar",
847
1025
  title: "Pause a Rank Radar",
848
1026
  description: "Use this to pause tracking on a Rank Radar without losing anything \u2014 the action the DataDive API calls 'archiving', so when a user asks to archive a Rank Radar, this is the tool they mean. Tracking stops for all of its keywords and the Daily Tracked Keywords quota they held is freed for other Rank Radars, while the Rank Radar and its ranking history are kept. Fully reversible with `resume_rank_radar`, so it needs no `confirm` \u2014 prefer it over `delete_rank_radar` whenever the user may want the data back. Paused Rank Radars show up in `list_rank_radars` under `status: PAUSED` \u2014 not `ARCHIVED`, which covers Rank Radars removed with `delete_rank_radar`. Safe to repeat: pausing an already paused Rank Radar succeeds and changes nothing.",
849
- inputSchema: inputSchema16,
1027
+ inputSchema: inputSchema21,
850
1028
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
851
1029
  handler: async (args, ctx) => {
852
1030
  await httpPost(
@@ -859,15 +1037,15 @@ var pauseRankRadarTool = {
859
1037
  };
860
1038
 
861
1039
  // src/tools/resume-rank-radar.ts
862
- import { z as z16 } from "zod";
863
- var inputSchema17 = {
864
- rankRadarId: z16.string().uuid().describe("The Rank Radar UUID to resume (from `list_rank_radars`).")
1040
+ import { z as z21 } from "zod";
1041
+ var inputSchema22 = {
1042
+ rankRadarId: z21.string().uuid().describe("The Rank Radar UUID to resume (from `list_rank_radars`).")
865
1043
  };
866
1044
  var resumeRankRadarTool = {
867
1045
  name: "resume_rank_radar",
868
1046
  title: "Resume a Paused Rank Radar",
869
1047
  description: "Use this to restart tracking on a Rank Radar that was paused with `pause_rank_radar` (shown by `list_rank_radars` under `status: PAUSED`; a deleted, `ARCHIVED` one cannot be resumed). Keywords are resumed as far as the available Daily Tracked Keywords quota allows, most relevant first \u2014 so with a tight quota only part of the original keyword set comes back; check `get_quota` first if that matters. Reversible with `pause_rank_radar`, so it needs no `confirm`. Fails with a bad-request error when there is no quota left at all. Safe to repeat: resuming an already active Rank Radar changes nothing.",
870
- inputSchema: inputSchema17,
1048
+ inputSchema: inputSchema22,
871
1049
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
872
1050
  handler: async (args, ctx) => {
873
1051
  await httpPost(
@@ -880,10 +1058,10 @@ var resumeRankRadarTool = {
880
1058
  };
881
1059
 
882
1060
  // src/tools/delete-rank-radar.ts
883
- import { z as z17 } from "zod";
884
- var inputSchema18 = {
885
- rankRadarId: z17.string().uuid().describe("The Rank Radar UUID to delete (from `list_rank_radars`)."),
886
- confirm: z17.boolean().optional().describe(
1061
+ import { z as z22 } from "zod";
1062
+ var inputSchema23 = {
1063
+ rankRadarId: z22.string().uuid().describe("The Rank Radar UUID to delete (from `list_rank_radars`)."),
1064
+ confirm: z22.boolean().optional().describe(
887
1065
  "Must be true to proceed \u2014 deleting a Rank Radar destroys its ranking history permanently. Confirm with the user first."
888
1066
  )
889
1067
  };
@@ -891,7 +1069,7 @@ var deleteRankRadarTool = {
891
1069
  name: "delete_rank_radar",
892
1070
  title: "Delete a Rank Radar",
893
1071
  description: "Use this to permanently remove a Rank Radar and all of its keyword ranking history. \u26A0\uFE0F Cannot be undone \u2014 there is no restore endpoint; set `confirm: true` only after the user approves. It does free the Daily Tracked Keywords quota those keywords held. Deleted Rank Radars are what `list_rank_radars` returns for `status: ARCHIVED`. If the user only wants to stop tracking for a while and keep the history \u2014 or asks to 'archive' the Rank Radar, which is what the DataDive API calls pausing \u2014 use `pause_rank_radar` instead: it frees the same quota and is reversible with `resume_rank_radar`.",
894
- inputSchema: inputSchema18,
1072
+ inputSchema: inputSchema23,
895
1073
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
896
1074
  handler: async (args, ctx) => {
897
1075
  const pending = requireConfirmation(
@@ -909,10 +1087,10 @@ var deleteRankRadarTool = {
909
1087
  };
910
1088
 
911
1089
  // src/tools/add-rank-radar-search-terms.ts
912
- import { z as z18 } from "zod";
913
- var inputSchema19 = {
914
- rankRadarId: z18.string().uuid().describe("The Rank Radar UUID to add the search terms to (from `list_rank_radars`)."),
915
- searchTerms: z18.array(z18.string().min(1)).min(1).max(ADD_SEARCH_TERMS_MAX).describe(
1090
+ import { z as z23 } from "zod";
1091
+ var inputSchema24 = {
1092
+ rankRadarId: z23.string().uuid().describe("The Rank Radar UUID to add the search terms to (from `list_rank_radars`)."),
1093
+ searchTerms: z23.array(z23.string().min(1)).min(1).max(ADD_SEARCH_TERMS_MAX).describe(
916
1094
  `The keywords to start tracking, 1\u2013${ADD_SEARCH_TERMS_MAX} per call. Must be unique. Amazon-ignored special characters (@, #, _, parentheses, \u2026) are rejected.`
917
1095
  )
918
1096
  };
@@ -920,7 +1098,7 @@ var addRankRadarSearchTermsTool = {
920
1098
  name: "add_rank_radar_search_terms",
921
1099
  title: "Add Search Terms to a Rank Radar",
922
1100
  description: "Use this to start tracking extra keywords on an existing Rank Radar, instead of creating a new one with `create_rank_radar`. New keywords are added to the niche and tracked from now on; any matching keyword that was previously paused is resumed. Each newly tracked keyword takes one Daily Tracked Keywords slot, and the call fails if the quota is exhausted \u2014 check `get_quota` first. Reversible with `pause_rank_radar_search_terms`, which frees the slots again, so it needs no `confirm`. Returns `originBreakdown` (where each submitted term ended up) and `keywordToRankRadarKeywordIdMap`, whose ids are what the pause/resume search-term tools take.",
923
- inputSchema: inputSchema19,
1101
+ inputSchema: inputSchema24,
924
1102
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
925
1103
  handler: async (args, ctx) => {
926
1104
  return await httpPost(
@@ -932,10 +1110,10 @@ var addRankRadarSearchTermsTool = {
932
1110
  };
933
1111
 
934
1112
  // src/tools/pause-rank-radar-search-terms.ts
935
- import { z as z19 } from "zod";
936
- var inputSchema20 = {
937
- rankRadarId: z19.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
938
- rankRadarKeywordIds: z19.array(z19.string().uuid()).min(1).describe(
1113
+ import { z as z24 } from "zod";
1114
+ var inputSchema25 = {
1115
+ rankRadarId: z24.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
1116
+ rankRadarKeywordIds: z24.array(z24.string().uuid()).min(1).describe(
939
1117
  "The Rank Radar keyword ids to pause. These are the `id` values from `get_rank_radar_data`, or the values of `keywordToRankRadarKeywordIdMap` returned by `add_rank_radar_search_terms` \u2014 not the keyword text."
940
1118
  )
941
1119
  };
@@ -943,7 +1121,7 @@ var pauseRankRadarSearchTermsTool = {
943
1121
  name: "pause_rank_radar_search_terms",
944
1122
  title: "Pause Search Terms of a Rank Radar",
945
1123
  description: "Use this to stop tracking individual keywords on a Rank Radar while keeping the Rank Radar itself active \u2014 the way to trim a keyword set the user finds too broad. The DataDive API calls this 'archiving' the search terms; nothing is lost. The paused keywords keep their history and their Daily Tracked Keywords slots are freed for other keywords. Reversible with `resume_rank_radar_search_terms`, so it needs no `confirm`. Takes keyword ids, not keyword text: get them from `get_rank_radar_data`. Safe to repeat: already-paused terms are left unchanged.",
946
- inputSchema: inputSchema20,
1124
+ inputSchema: inputSchema25,
947
1125
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
948
1126
  handler: async (args, ctx) => {
949
1127
  await httpPost(
@@ -960,10 +1138,10 @@ var pauseRankRadarSearchTermsTool = {
960
1138
  };
961
1139
 
962
1140
  // src/tools/resume-rank-radar-search-terms.ts
963
- import { z as z20 } from "zod";
964
- var inputSchema21 = {
965
- rankRadarId: z20.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
966
- rankRadarKeywordIds: z20.array(z20.string().uuid()).min(1).describe(
1141
+ import { z as z25 } from "zod";
1142
+ var inputSchema26 = {
1143
+ rankRadarId: z25.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
1144
+ rankRadarKeywordIds: z25.array(z25.string().uuid()).min(1).describe(
967
1145
  "The Rank Radar keyword ids to resume. These are the `id` values from `get_rank_radar_data`, or the values of `keywordToRankRadarKeywordIdMap` returned by `add_rank_radar_search_terms` \u2014 not the keyword text."
968
1146
  )
969
1147
  };
@@ -971,7 +1149,7 @@ var resumeRankRadarSearchTermsTool = {
971
1149
  name: "resume_rank_radar_search_terms",
972
1150
  title: "Resume Paused Search Terms of a Rank Radar",
973
1151
  description: "Use this to start tracking keywords again that were paused with `pause_rank_radar_search_terms`. Each resumed keyword takes back one Daily Tracked Keywords slot, and the call fails if the quota is exhausted \u2014 check `get_quota` first. Reversible with `pause_rank_radar_search_terms`, so it needs no `confirm`. Takes keyword ids, not keyword text: get them from `get_rank_radar_data`. Safe to repeat: already-active terms are left unchanged.",
974
- inputSchema: inputSchema21,
1152
+ inputSchema: inputSchema26,
975
1153
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
976
1154
  handler: async (args, ctx) => {
977
1155
  await httpPost(
@@ -988,16 +1166,16 @@ var resumeRankRadarSearchTermsTool = {
988
1166
  };
989
1167
 
990
1168
  // src/tools/generate-listing-copy.ts
991
- import { z as z21 } from "zod";
992
- var inputSchema22 = {
993
- nicheId: z21.string().min(1).describe("The niche whose keyword data the copy is written from. Get one from `list_niches`."),
994
- strategy: z21.enum(LISTING_COPY_STRATEGIES).describe(
1169
+ import { z as z26 } from "zod";
1170
+ var inputSchema27 = {
1171
+ nicheId: z26.string().min(1).describe("The niche whose keyword data the copy is written from. Get one from `list_niches`."),
1172
+ strategy: z26.enum(LISTING_COPY_STRATEGIES).describe(
995
1173
  "Which generation strategy to use. `cosmo` targets Amazon's COSMO relevance model, `ranking-juice` optimises for DataDive's ranking-juice score, `nlp` writes for keyword coverage, and `cosmo-rufus` targets COSMO plus the Rufus shopping assistant. Ask the user if they have no preference."
996
1174
  ),
997
- currentListing: z21.record(z21.union([z21.string(), z21.array(z21.string())])).describe(
1175
+ currentListing: z26.record(z26.union([z26.string(), z26.array(z26.string())])).describe(
998
1176
  'The seller\'s existing listing to rewrite, as an object \u2014 e.g. `{ "title": "...", "description": "...", "bullets": ["...", "..."] }`. Pass what the user has; it is the starting point the generated copy builds on.'
999
1177
  ),
1000
- confirm: z21.boolean().optional().describe(
1178
+ confirm: z26.boolean().optional().describe(
1001
1179
  "Must be true to proceed \u2014 each generation spends one AI Copywriter prompt from the quota. Confirm the cost with the user first."
1002
1180
  )
1003
1181
  };
@@ -1005,7 +1183,7 @@ var generateListingCopyTool = {
1005
1183
  name: "generate_listing_copy",
1006
1184
  title: "Generate Listing Copy for a Niche",
1007
1185
  description: "Use this to draft an optimised Amazon listing \u2014 title, bullets and description \u2014 from a niche's keyword research and the seller's current listing. It writes text only: nothing is published to Amazon, and the user still has to paste the result into Seller Central. \u26A0\uFE0F Spends one AI Copywriter prompt from the quota per call and cannot be undone \u2014 set `confirm: true` only after the user approves. Not safe to retry: each call spends another prompt, so if a call errors or times out, poll `get_listing_copy_generation_status` instead of re-calling. Runs asynchronously: returns a `generationId` \u2014 poll `get_listing_copy_generation_status` with it until the status is `complete`.",
1008
- inputSchema: inputSchema22,
1186
+ inputSchema: inputSchema27,
1009
1187
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
1010
1188
  handler: async (args, ctx) => {
1011
1189
  const pending = requireConfirmation(
@@ -1023,16 +1201,16 @@ var generateListingCopyTool = {
1023
1201
  };
1024
1202
 
1025
1203
  // src/tools/get-listing-copy-generation-status.ts
1026
- import { z as z22 } from "zod";
1027
- var inputSchema23 = {
1028
- nicheId: z22.string().min(1).describe("The niche the generation was started for."),
1029
- generationId: z22.string().min(1).describe("The `generationId` returned by `generate_listing_copy`.")
1204
+ import { z as z27 } from "zod";
1205
+ var inputSchema28 = {
1206
+ nicheId: z27.string().min(1).describe("The niche the generation was started for."),
1207
+ generationId: z27.string().min(1).describe("The `generationId` returned by `generate_listing_copy`.")
1030
1208
  };
1031
1209
  var getListingCopyGenerationStatusTool = {
1032
1210
  name: "get_listing_copy_generation_status",
1033
1211
  title: "Get Listing Copy Generation Status",
1034
1212
  description: "Use this to poll a listing-copy draft started with `generate_listing_copy` until it finishes. Returns one of three shapes keyed by `status`: `generating` (still running \u2014 poll again in a few seconds), `complete` (with `result`, carrying the generated `title`, `bullets`, `description`, `itemHighlights` and its `rankingJuice` score), or `failed` (with an `error` message). Polling is free \u2014 it does not spend another AI Copywriter prompt, so always poll rather than re-calling `generate_listing_copy`.",
1035
- inputSchema: inputSchema23,
1213
+ inputSchema: inputSchema28,
1036
1214
  annotations: { readOnlyHint: true },
1037
1215
  handler: async (args, ctx) => {
1038
1216
  return await httpGet(
@@ -1043,16 +1221,16 @@ var getListingCopyGenerationStatusTool = {
1043
1221
  };
1044
1222
 
1045
1223
  // src/tools/list-seller-profiles.ts
1046
- import { z as z23 } from "zod";
1047
- var inputSchema24 = {
1048
- currentPage: z23.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1049
- pageSize: z23.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1224
+ import { z as z28 } from "zod";
1225
+ var inputSchema29 = {
1226
+ currentPage: z28.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1227
+ pageSize: z28.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1050
1228
  };
1051
1229
  var listSellerProfilesTool = {
1052
1230
  name: "list_seller_profiles",
1053
1231
  title: "List Connected Amazon Seller Profiles",
1054
1232
  description: 'Use this when the user asks which Amazon seller accounts are connected, or as the discovery step to find the `sellerId` and `marketplace` required by `get_asin_inventory_distribution`, `get_seller_catalog`, `get_seller_listing_changes`, and the alert tools. Returns a paginated list of the organization\'s connected seller profiles \u2014 each item has sellerId, sellerName, marketplace (e.g. "com", "co.uk"), hasAdApi (whether Advertising API credentials are connected), and createdAt \u2014 plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).',
1055
- inputSchema: inputSchema24,
1233
+ inputSchema: inputSchema29,
1056
1234
  annotations: { readOnlyHint: true },
1057
1235
  handler: async (args, ctx) => {
1058
1236
  return await httpGet(
@@ -1067,15 +1245,15 @@ var listSellerProfilesTool = {
1067
1245
  };
1068
1246
 
1069
1247
  // src/tools/get-seller-catalog.ts
1070
- import { z as z25 } from "zod";
1248
+ import { z as z30 } from "zod";
1071
1249
 
1072
1250
  // src/tools/seller-scoped.ts
1073
- import { z as z24 } from "zod";
1251
+ import { z as z29 } from "zod";
1074
1252
  var sellerScopedInputSchema = {
1075
- sellerId: z24.string().min(1).describe(
1253
+ sellerId: z29.string().min(1).describe(
1076
1254
  "Amazon seller account ID. Get it from `list_seller_profiles`, or find it on the Connections page at https://2.datadive.tools."
1077
1255
  ),
1078
- marketplace: z24.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.')
1256
+ marketplace: z29.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.')
1079
1257
  };
1080
1258
  function sellerMarketplacePath(sellerId, marketplace, suffix) {
1081
1259
  return `/v1/seller_profiles/${encodeURIComponent(sellerId)}/marketplaces/${encodeURIComponent(
@@ -1084,19 +1262,19 @@ function sellerMarketplacePath(sellerId, marketplace, suffix) {
1084
1262
  }
1085
1263
 
1086
1264
  // src/tools/get-seller-catalog.ts
1087
- var inputSchema25 = {
1265
+ var inputSchema30 = {
1088
1266
  ...sellerScopedInputSchema,
1089
- search: z25.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1090
- brand: z25.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1091
- status: z25.enum(CATALOG_STATUSES).optional().describe('Listing status filter. "Active" (default) or "all".'),
1092
- currentPage: z25.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1093
- pageSize: z25.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1267
+ search: z30.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1268
+ brand: z30.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1269
+ status: z30.enum(CATALOG_STATUSES).optional().describe('Listing status filter. "Active" (default) or "all".'),
1270
+ currentPage: z30.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1271
+ pageSize: z30.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1094
1272
  };
1095
1273
  var getSellerCatalogTool = {
1096
1274
  name: "get_seller_catalog",
1097
1275
  title: "List Catalog ASINs for a Seller",
1098
1276
  description: "Use this when the user wants to browse or search a seller's own Amazon catalog \u2014 the products they sell on a given marketplace. Requires a `sellerId` + `marketplace` (use `list_seller_profiles` to discover them). Filter by `search` (title/brand), `brand`, and `status` (Active by default, or all). Returns a paginated list where each item has asin, title, parentAsin, brand, status, imageUrl, and hasVariations, plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
1099
- inputSchema: inputSchema25,
1277
+ inputSchema: inputSchema30,
1100
1278
  annotations: { readOnlyHint: true },
1101
1279
  handler: async (args, ctx) => {
1102
1280
  return await httpGet(
@@ -1114,28 +1292,28 @@ var getSellerCatalogTool = {
1114
1292
  };
1115
1293
 
1116
1294
  // src/tools/get-seller-listing-changes.ts
1117
- import { z as z26 } from "zod";
1118
- var inputSchema26 = {
1295
+ import { z as z31 } from "zod";
1296
+ var inputSchema31 = {
1119
1297
  ...sellerScopedInputSchema,
1120
- types: z26.array(z26.enum(LISTING_CHANGE_TYPES)).min(1).optional().describe('Filter to specific change types. Omit for all. Any of "Price", "Content", "Image".'),
1121
- asin: z26.string().min(1).optional().describe("Filter to an exact ASIN or parent ASIN."),
1122
- brand: z26.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1123
- search: z26.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1124
- startDate: z26.string().regex(ISO_8601, "startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)").optional().describe("ISO-8601 date/timestamp; return only changes detected on or after this time."),
1125
- endDate: z26.string().regex(ISO_8601, "endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)").optional().describe("ISO-8601 date/timestamp; return only changes detected on or before this time."),
1126
- sortBy: z26.enum(LISTING_CHANGE_SORT_BY).optional().describe('Sort field. "date" (default) or "type".'),
1127
- sortOrder: z26.enum(SORT_ORDER).optional().describe('Sort direction. "DESC" (default) or "ASC".'),
1128
- includeCorrelations: z26.boolean().optional().describe(
1298
+ types: z31.array(z31.enum(LISTING_CHANGE_TYPES)).min(1).optional().describe('Filter to specific change types. Omit for all. Any of "Price", "Content", "Image".'),
1299
+ asin: z31.string().min(1).optional().describe("Filter to an exact ASIN or parent ASIN."),
1300
+ brand: z31.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1301
+ search: z31.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1302
+ startDate: z31.string().regex(ISO_8601, "startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)").optional().describe("ISO-8601 date/timestamp; return only changes detected on or after this time."),
1303
+ endDate: z31.string().regex(ISO_8601, "endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)").optional().describe("ISO-8601 date/timestamp; return only changes detected on or before this time."),
1304
+ sortBy: z31.enum(LISTING_CHANGE_SORT_BY).optional().describe('Sort field. "date" (default) or "type".'),
1305
+ sortOrder: z31.enum(SORT_ORDER).optional().describe('Sort direction. "DESC" (default) or "ASC".'),
1306
+ includeCorrelations: z31.boolean().optional().describe(
1129
1307
  "When true, include the ranking/conversion `correlation` object per change (CVR and search-term movement before vs after). Defaults to false."
1130
1308
  ),
1131
- currentPage: z26.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1132
- pageSize: z26.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1309
+ currentPage: z31.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1310
+ pageSize: z31.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1133
1311
  };
1134
1312
  var getSellerListingChangesTool = {
1135
1313
  name: "get_seller_listing_changes",
1136
1314
  title: "List Listing Changes for a Seller",
1137
1315
  description: "Use this when the user asks what changed on their Amazon listings \u2014 price, content (title/bullets/description), or image edits \u2014 for a seller account. Requires a `sellerId` + `marketplace` (use `list_seller_profiles` to discover them). Filter by `types`, `asin`/parent ASIN, `brand`, `search`, and a `startDate`/`endDate` range; sort with `sortBy`/`sortOrder`. Set `includeCorrelations: true` to attach the ranking/conversion impact per change. Returns a paginated list where each item has asin, title, imageUrl, date, type, contentType, description, previousValue, newValue, and (optionally) correlation, plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
1138
- inputSchema: inputSchema26,
1316
+ inputSchema: inputSchema31,
1139
1317
  annotations: { readOnlyHint: true },
1140
1318
  handler: async (args, ctx) => {
1141
1319
  return await httpGet(
@@ -1160,40 +1338,40 @@ var getSellerListingChangesTool = {
1160
1338
  };
1161
1339
 
1162
1340
  // src/tools/list-ppc-campaigns.ts
1163
- import { z as z27 } from "zod";
1341
+ import { z as z32 } from "zod";
1164
1342
  var PPC_CAMPAIGNS_MAX_WINDOW_DAYS = 90;
1165
1343
  var PPC_CAMPAIGNS_MAX_PAGE_SIZE = 50;
1166
- var inputSchema27 = {
1344
+ var inputSchema32 = {
1167
1345
  ...sellerScopedInputSchema,
1168
- startDate: z27.string().regex(
1346
+ startDate: z32.string().regex(
1169
1347
  ISO_8601,
1170
1348
  "startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)"
1171
1349
  ).optional().describe("Start of the reporting window, ISO-8601. Defaults to 30 days before endDate."),
1172
- endDate: z27.string().regex(
1350
+ endDate: z32.string().regex(
1173
1351
  ISO_8601,
1174
1352
  "endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)"
1175
1353
  ).optional().describe(
1176
1354
  `End of the reporting window, ISO-8601. Defaults to now. At most ${PPC_CAMPAIGNS_MAX_WINDOW_DAYS} days after startDate.`
1177
1355
  ),
1178
- asin: z27.string().min(1).optional().describe("Only campaigns advertising this ASIN. Cannot be combined with parentAsin."),
1179
- parentAsin: z27.string().min(1).optional().describe("Only campaigns advertising any ASIN of this variation family. Cannot be combined with asin."),
1180
- search: z27.string().min(3).max(200).optional().describe("Partial match on campaign name or advertised ASIN. At least 3 characters."),
1181
- includePlacements: z27.boolean().optional().describe(
1356
+ asin: z32.string().min(1).optional().describe("Only campaigns advertising this ASIN. Cannot be combined with parentAsin."),
1357
+ parentAsin: z32.string().min(1).optional().describe("Only campaigns advertising any ASIN of this variation family. Cannot be combined with asin."),
1358
+ search: z32.string().min(3).max(200).optional().describe("Partial match on campaign name or advertised ASIN. At least 3 characters."),
1359
+ includePlacements: z32.boolean().optional().describe(
1182
1360
  "Include the per-placement breakdown (topOfSearch, restOfSearch, productPage, offAmazon, each with its bid adjustment). Defaults to true; set false for a smaller response when placements are not needed."
1183
1361
  ),
1184
- sortBy: z27.enum([...PPC_CAMPAIGN_BASE_SORT_BY, ...PPC_CAMPAIGN_PLACEMENT_SORT_BY]).optional().describe(
1362
+ sortBy: z32.enum([...PPC_CAMPAIGN_BASE_SORT_BY, ...PPC_CAMPAIGN_PLACEMENT_SORT_BY]).optional().describe(
1185
1363
  `Sort field. Defaults to "name". Any of ${PPC_CAMPAIGN_BASE_SORT_BY.join(", ")}. The placement fields (e.g. "tosSpend", "tosAcos", "ppBidAdjustment") are only accepted when includePlacements is not false.`
1186
1364
  ),
1187
- sortOrder: z27.enum(SORT_ORDER).optional().describe('Sort direction. "ASC" (default) or "DESC".'),
1188
- state: z27.enum(PPC_CAMPAIGN_STATES).optional().describe("Only ENABLED or only PAUSED campaigns. Omit for both."),
1189
- currentPage: z27.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1190
- pageSize: z27.number().int().min(1).max(PPC_CAMPAIGNS_MAX_PAGE_SIZE).optional().describe(`Campaigns per page (max ${PPC_CAMPAIGNS_MAX_PAGE_SIZE}). Defaults to 20.`)
1365
+ sortOrder: z32.enum(SORT_ORDER).optional().describe('Sort direction. "ASC" (default) or "DESC".'),
1366
+ state: z32.enum(PPC_CAMPAIGN_STATES).optional().describe("Only ENABLED or only PAUSED campaigns. Omit for both."),
1367
+ currentPage: z32.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1368
+ pageSize: z32.number().int().min(1).max(PPC_CAMPAIGNS_MAX_PAGE_SIZE).optional().describe(`Campaigns per page (max ${PPC_CAMPAIGNS_MAX_PAGE_SIZE}). Defaults to 20.`)
1191
1369
  };
1192
1370
  var listPpcCampaignsTool = {
1193
1371
  name: "list_ppc_campaigns",
1194
1372
  title: "List PPC Campaigns for a Seller",
1195
1373
  description: `Use this when the user asks about their Sponsored Products campaigns \u2014 which campaigns spend the most, which have a high ACOS, how placements (top of search, rest of search, product pages) perform, or which campaigns advertise a given product. Requires a \`sellerId\` + \`marketplace\` (use \`list_seller_profiles\` to discover them). Narrow to one product with \`asin\`, or to a variation family with \`parentAsin\` (not both). The reporting window defaults to the last 30 days and cannot exceed ${PPC_CAMPAIGNS_MAX_WINDOW_DAYS} days. Returns a paginated list where each campaign has campaignId, name, type, state, targetingType, bidStrategy, budget { type, amount, currencyCode }, window totals (impressions, clicks, ctr, cpc, cvr, spend, unitsSold, orders, sales, tosImpressionShare, acos, roas, tacos), asinCount and, unless includePlacements is false, \`placements\`; plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev). To find the biggest spenders, sort with \`sortBy: "spend", sortOrder: "DESC"\` rather than paging through every campaign. Each call counts toward API usage and is rate limited (about 60 requests/minute). For per-keyword ad data of a Rank Radar, use \`get_rank_radar_ppc_data\`.`,
1196
- inputSchema: inputSchema27,
1374
+ inputSchema: inputSchema32,
1197
1375
  annotations: { readOnlyHint: true },
1198
1376
  handler: async (args, ctx) => {
1199
1377
  if (args.asin && args.parentAsin) {
@@ -1224,13 +1402,13 @@ var listPpcCampaignsTool = {
1224
1402
  };
1225
1403
 
1226
1404
  // src/tools/get-asin-inventory-distribution.ts
1227
- import { z as z28 } from "zod";
1228
- var inputSchema28 = {
1229
- sellerId: z28.string().min(1).describe(
1405
+ import { z as z33 } from "zod";
1406
+ var inputSchema33 = {
1407
+ sellerId: z33.string().min(1).describe(
1230
1408
  "Amazon seller account ID as connected to DataDive. Get it from `list_seller_profiles`, or find it on the Connections page at https://2.datadive.tools."
1231
1409
  ),
1232
- marketplace: z28.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
1233
- asin: z28.string().regex(/^[A-Z0-9]{10}$/, "ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).").describe(
1410
+ marketplace: z33.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
1411
+ asin: z33.string().regex(/^[A-Z0-9]{10}$/, "ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).").describe(
1234
1412
  "Amazon Standard Identification Number (10 uppercase alphanumeric characters, e.g. B08XYZ1234)."
1235
1413
  )
1236
1414
  };
@@ -1238,7 +1416,7 @@ var getAsinInventoryDistributionTool = {
1238
1416
  name: "get_asin_inventory_distribution",
1239
1417
  title: "Get Per-Fulfillment-Center Inventory Distribution for an ASIN",
1240
1418
  description: "Use this when the user asks about inventory levels, stock by fulfillment center, or where their units are sitting across Amazon's fulfillment network for a specific ASIN. Requires the Amazon sellerId (from `list_seller_profiles` or the user's DataDive Connections page at https://2.datadive.tools), the marketplace code, and the ASIN. Returns totalSellableUnits and a per-FC `distribution` array (fc, state, availableStock, availableStockPercentage). `lastUpdatedAt` may be null when no successful inventory ingestion has occurred in the last 30 days.",
1241
- inputSchema: inputSchema28,
1419
+ inputSchema: inputSchema33,
1242
1420
  annotations: { readOnlyHint: true },
1243
1421
  handler: async (args, ctx) => {
1244
1422
  return await httpGet(
@@ -1251,18 +1429,18 @@ var getAsinInventoryDistributionTool = {
1251
1429
  };
1252
1430
 
1253
1431
  // src/tools/alert-query.ts
1254
- import { z as z29 } from "zod";
1432
+ import { z as z34 } from "zod";
1255
1433
  var alertsQueryInputSchema = {
1256
- sellerId: z29.string().min(1).max(50).optional().describe('Filter to a single connected Amazon seller account ID, e.g. "A1B2C3D4E5".'),
1257
- marketplace: z29.enum(SUPPORTED_MARKETPLACES).optional().describe('Filter to a single Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
1258
- status: z29.enum(ALERT_STATUSES).optional().describe(
1434
+ sellerId: z34.string().min(1).max(50).optional().describe('Filter to a single connected Amazon seller account ID, e.g. "A1B2C3D4E5".'),
1435
+ marketplace: z34.enum(SUPPORTED_MARKETPLACES).optional().describe('Filter to a single Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
1436
+ status: z34.enum(ALERT_STATUSES).optional().describe(
1259
1437
  "Lifecycle filter. `active` (default) returns unresolved alerts, `resolved` returns resolved alerts, `all` returns both. Dismissed alerts are never returned."
1260
1438
  ),
1261
- updatedSince: z29.string().regex(ISO_8601, "updatedSince must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)").optional().describe(
1439
+ updatedSince: z34.string().regex(ISO_8601, "updatedSince must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)").optional().describe(
1262
1440
  "ISO-8601 timestamp; return only alerts surfaced at or after this time (filters on `lastAlertedAt`). Use it to incrementally sync since your last poll. Defaults to the last 30 days when omitted."
1263
1441
  ),
1264
- currentPage: z29.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1265
- pageSize: z29.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1442
+ currentPage: z34.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1443
+ pageSize: z34.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1266
1444
  };
1267
1445
 
1268
1446
  // src/tools/list-indexing-issue-alerts.ts
@@ -1312,12 +1490,12 @@ var listBlindSpendAlertsTool = {
1312
1490
  };
1313
1491
 
1314
1492
  // src/tools/get-quota.ts
1315
- var inputSchema29 = {};
1493
+ var inputSchema34 = {};
1316
1494
  var getQuotaTool = {
1317
1495
  name: "get_quota",
1318
1496
  title: "Get Quota Usage",
1319
1497
  description: "Use this when the user asks how much quota or how many tokens they have left, or before creating a Niche Dive or Rank Radar to check remaining headroom. Returns current quota usage and capacity for each billable feature \u2014 DIVED_ASINS (Dive tokens), RANK_RADAR_KEYWORDS (tracked keywords), PRODUCT_BRIEF_ASINS, and AI_COPYWRITER_PROMPTS \u2014 each with `used` and `capacity` (null when unlimited / not applicable), plus `nextRefreshDate` (ISO-8601 timestamp of the next quota reset, or null). Takes no arguments.",
1320
- inputSchema: inputSchema29,
1498
+ inputSchema: inputSchema34,
1321
1499
  annotations: { readOnlyHint: true },
1322
1500
  handler: async (_args, ctx) => {
1323
1501
  return await httpGet({ config: ctx.config, toolName: "get_quota" }, "/v1/quota");
@@ -1325,20 +1503,20 @@ var getQuotaTool = {
1325
1503
  };
1326
1504
 
1327
1505
  // src/tools/list-usage.ts
1328
- import { z as z30 } from "zod";
1329
- var inputSchema30 = {
1330
- type: z30.enum(BILLABLE_FEATURE_TYPES).optional().describe("Filter to a single billable feature type. Omit to return all types."),
1331
- search: z30.string().min(1).optional().describe("Case-insensitive partial match on the user's name or email."),
1332
- startDate: z30.string().regex(ISO_8601, "startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)").optional().describe("ISO-8601 date/timestamp; return only usage logs recorded on or after this time."),
1333
- endDate: z30.string().regex(ISO_8601, "endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)").optional().describe("ISO-8601 date/timestamp; return only usage logs recorded on or before this time."),
1334
- currentPage: z30.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1335
- pageSize: z30.number().int().min(1).max(200).optional().describe("Items per page (max 200). Defaults to 50.")
1506
+ import { z as z35 } from "zod";
1507
+ var inputSchema35 = {
1508
+ type: z35.enum(BILLABLE_FEATURE_TYPES).optional().describe("Filter to a single billable feature type. Omit to return all types."),
1509
+ search: z35.string().min(1).optional().describe("Case-insensitive partial match on the user's name or email."),
1510
+ startDate: z35.string().regex(ISO_8601, "startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)").optional().describe("ISO-8601 date/timestamp; return only usage logs recorded on or after this time."),
1511
+ endDate: z35.string().regex(ISO_8601, "endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)").optional().describe("ISO-8601 date/timestamp; return only usage logs recorded on or before this time."),
1512
+ currentPage: z35.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1513
+ pageSize: z35.number().int().min(1).max(200).optional().describe("Items per page (max 200). Defaults to 50.")
1336
1514
  };
1337
1515
  var listUsageTool = {
1338
1516
  name: "list_usage",
1339
1517
  title: "List Billable Usage Logs",
1340
1518
  description: "Use this when the user asks who consumed tokens, how their quota was spent, or wants an audit of billable activity over a date range. Retrieves a paginated list of billable feature usage logs for the organization \u2014 each entry is a token-consumption event (a dive, rank-radar creation, AI copywriter prompt, etc.). Filter by `type` (billable feature), `search` (user name/email), and `startDate`/`endDate`. Each item includes name, email, qty (tokens consumed), type, action (specific operation), nicheId/nicheName, rankRadarId, and date, plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
1341
- inputSchema: inputSchema30,
1519
+ inputSchema: inputSchema35,
1342
1520
  annotations: { readOnlyHint: true },
1343
1521
  handler: async (args, ctx) => {
1344
1522
  return await httpGet(
@@ -1360,6 +1538,7 @@ var listUsageTool = {
1360
1538
  var allTools = [
1361
1539
  listNichesTool,
1362
1540
  getNicheKeywordsTool,
1541
+ listKeywordExclusionsTool,
1363
1542
  getNicheRootsTool,
1364
1543
  getNicheCompetitorsTool,
1365
1544
  getRankingJuiceTool,
@@ -1372,6 +1551,10 @@ var allTools = [
1372
1551
  createNicheDiveFromCompetitorsListTool,
1373
1552
  rediveNicheTool,
1374
1553
  getDiveStatusTool,
1554
+ addNicheKeywordsTool,
1555
+ removeNicheKeywordsTool,
1556
+ addKeywordExclusionsTool,
1557
+ removeKeywordExclusionsTool,
1375
1558
  addRankRadarSearchTermsTool,
1376
1559
  pauseRankRadarSearchTermsTool,
1377
1560
  resumeRankRadarSearchTermsTool,
@@ -1394,6 +1577,14 @@ var allTools = [
1394
1577
 
1395
1578
  // src/whats-new.ts
1396
1579
  var WHATS_NEW = [
1580
+ {
1581
+ version: "0.17.0",
1582
+ released: "2026-10-12",
1583
+ items: [
1584
+ "Add your own keywords to a niche's Master Keyword List, or take them back out (`add_niche_keywords`, `remove_niche_keywords`). Free while the niche is fresh; on an older niche you are asked to confirm the Dive tokens first. `get_niche_keywords` now shows where each keyword came from.",
1585
+ "See and change which keywords a niche hides (`list_keyword_exclusions`, `add_keyword_exclusions`, `remove_keyword_exclusions`): exclude one exact keyword, or every keyword containing a phrase. Free."
1586
+ ]
1587
+ },
1397
1588
  {
1398
1589
  version: "0.16.1",
1399
1590
  released: "2026-09-29",
@@ -1438,8 +1629,8 @@ var WHATS_NEW = [
1438
1629
  }
1439
1630
  ];
1440
1631
  var INSTRUCTIONS_RELEASES = 2;
1441
- function formatEntries(entries) {
1442
- return entries.map((e) => `${e.released} (v${e.version}):
1632
+ function formatEntries(entries2) {
1633
+ return entries2.map((e) => `${e.released} (v${e.version}):
1443
1634
  ${e.items.map((i) => `- ${i}`).join("\n")}`).join("\n\n");
1444
1635
  }
1445
1636
  var SERVER_INSTRUCTIONS = `DataDive tools for Amazon sellers: niche research (niches, keywords, roots, competitors, Ranking Juice), Rank Radar keyword rank tracking with SQP and PPC data, the seller's catalog, listing changes, inventory, PPC campaigns and alerts. Start with a discovery tool: \`list_niches\`, \`list_rank_radars\` or \`list_seller_profiles\`. Tools that spend tokens ask for confirmation first.