@datadive-tools/mcp 0.16.0 → 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.0";
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,14 +532,42 @@ function rankRadarKeywordQuery(args) {
509
532
  }
510
533
 
511
534
  // src/tools/get-rank-radar-data.ts
512
- var inputSchema7 = rankRadarKeywordQueryInputSchema;
535
+ var inputSchema8 = rankRadarKeywordQueryInputSchema;
536
+ async function readAllKeywords(args, ctx) {
537
+ const request = { config: ctx.config, toolName: "get_rank_radar_data" };
538
+ const path = rankRadarPath(args.rankRadarId);
539
+ const keywords = [];
540
+ let page;
541
+ let currentPage = 1;
542
+ do {
543
+ page = await httpGet(
544
+ request,
545
+ path,
546
+ rankRadarKeywordQuery({ ...args, currentPage, pageSize: RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE })
547
+ );
548
+ keywords.push(...page.data ?? []);
549
+ currentPage++;
550
+ } while (page.hasNext && currentPage <= page.lastPage && (page.data?.length ?? 0) > 0);
551
+ return {
552
+ data: keywords,
553
+ currentPage: 1,
554
+ pageSize: keywords.length,
555
+ total: page.total,
556
+ lastPage: 1,
557
+ hasNext: false,
558
+ hasPrev: false
559
+ };
560
+ }
513
561
  var getRankRadarDataTool = {
514
562
  name: "get_rank_radar_data",
515
563
  title: "Get Keyword Rankings for a Rank Radar",
516
- 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. ${RANK_RADAR_KEYWORD_PAGING_NOTE} 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\`.`,
517
- inputSchema: inputSchema7,
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\`.`,
565
+ inputSchema: inputSchema8,
518
566
  annotations: { readOnlyHint: true },
519
567
  handler: async (args, ctx) => {
568
+ if (args.currentPage === void 0 && args.pageSize === void 0) {
569
+ return await readAllKeywords(args, ctx);
570
+ }
520
571
  return await httpGet(
521
572
  { config: ctx.config, toolName: "get_rank_radar_data" },
522
573
  rankRadarPath(args.rankRadarId),
@@ -526,12 +577,12 @@ var getRankRadarDataTool = {
526
577
  };
527
578
 
528
579
  // src/tools/get-rank-radar-sqp-data.ts
529
- var inputSchema8 = rankRadarKeywordQueryInputSchema;
580
+ var inputSchema9 = rankRadarKeywordQueryInputSchema;
530
581
  var getRankRadarSqpDataTool = {
531
582
  name: "get_rank_radar_sqp_data",
532
583
  title: "Get Search Query Performance for a Rank Radar",
533
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\`.`,
534
- inputSchema: inputSchema8,
585
+ inputSchema: inputSchema9,
535
586
  annotations: { readOnlyHint: true },
536
587
  handler: async (args, ctx) => {
537
588
  return await httpGet(
@@ -543,10 +594,10 @@ var getRankRadarSqpDataTool = {
543
594
  };
544
595
 
545
596
  // src/tools/get-rank-radar-ppc-data.ts
546
- import { z as z8 } from "zod";
547
- var inputSchema9 = {
597
+ import { z as z9 } from "zod";
598
+ var inputSchema10 = {
548
599
  ...rankRadarKeywordQueryInputSchema,
549
- includeCampaigns: z8.boolean().optional().describe(
600
+ includeCampaigns: z9.boolean().optional().describe(
550
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."
551
602
  )
552
603
  };
@@ -554,7 +605,7 @@ var getRankRadarPpcDataTool = {
554
605
  name: "get_rank_radar_ppc_data",
555
606
  title: "Get PPC Data for a Rank Radar",
556
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\`.`,
557
- inputSchema: inputSchema9,
608
+ inputSchema: inputSchema10,
558
609
  annotations: { readOnlyHint: true },
559
610
  handler: async (args, ctx) => {
560
611
  return await httpGet(
@@ -566,7 +617,7 @@ var getRankRadarPpcDataTool = {
566
617
  };
567
618
 
568
619
  // src/tools/create-rank-radar.ts
569
- import { z as z9 } from "zod";
620
+ import { z as z10 } from "zod";
570
621
 
571
622
  // src/tools/confirm.ts
572
623
  function needsConfirmation(confirm, ctx) {
@@ -587,11 +638,11 @@ function requireConfirmation(confirm, ctx, costNote) {
587
638
 
588
639
  // src/tools/create-rank-radar.ts
589
640
  var PATH = "/v1/niches/rank-radars";
590
- var inputSchema10 = {
591
- asin: z9.string().min(1).describe("The ASIN to track keyword rankings for."),
592
- numberOfKeywords: z9.number().int().min(1).describe("How many keywords to track (minimum 1). More keywords = broader tracking but more Search Term tokens."),
593
- nicheId: z9.string().min(1).describe("The niche this Rank Radar belongs to. Get one from `list_niches`."),
594
- 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(
595
646
  "Must be true to proceed \u2014 creating a Rank Radar spends Search Term tokens. Confirm the cost with the user first."
596
647
  )
597
648
  };
@@ -599,7 +650,7 @@ var createRankRadarTool = {
599
650
  name: "create_rank_radar",
600
651
  title: "Create a Rank Radar",
601
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`.",
602
- inputSchema: inputSchema10,
653
+ inputSchema: inputSchema11,
603
654
  // destructiveHint is technically "additive" here (this creates a new Rank Radar, it
604
655
  // doesn't destroy data), but we flag it destructive so clients prompt before the
605
656
  // irreversible token spend. The confirm gate enforces this server-side regardless.
@@ -624,20 +675,20 @@ var createRankRadarTool = {
624
675
  };
625
676
 
626
677
  // src/tools/create-niche-dive.ts
627
- import { z as z10 } from "zod";
628
- var inputSchema11 = {
629
- marketplace: z10.enum(SUPPORTED_MARKETPLACES).describe(
678
+ import { z as z11 } from "zod";
679
+ var inputSchema12 = {
680
+ marketplace: z11.enum(SUPPORTED_MARKETPLACES).describe(
630
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`)."
631
682
  ),
632
- asin: z10.string().min(1).describe("The seed ASIN to start the dive from (e.g. B08N5WRWNW). Competitors are built around it."),
633
- numberOfCompetitors: z10.number().int().min(2).describe("How many competitors to analyze (minimum 2). More competitors = deeper niche but more dive tokens."),
634
- 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.")
635
686
  };
636
687
  var createNicheDiveTool = {
637
688
  name: "create_niche_dive",
638
689
  title: "Create a Niche Dive",
639
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).",
640
- inputSchema: inputSchema11,
691
+ inputSchema: inputSchema12,
641
692
  // destructiveHint is technically "additive" here (a dive creates a new niche, it
642
693
  // doesn't destroy data), but we flag it destructive so clients prompt before the
643
694
  // irreversible token spend. The confirm gate enforces this server-side regardless.
@@ -662,20 +713,20 @@ var createNicheDiveTool = {
662
713
  };
663
714
 
664
715
  // src/tools/create-niche-dive-from-competitors-list.ts
665
- import { z as z11 } from "zod";
716
+ import { z as z12 } from "zod";
666
717
  var DIVE_FROM_COMPETITORS_MIN_ASINS = 2;
667
718
  var DIVE_FROM_COMPETITORS_MAX_ASINS = 200;
668
- 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, {
669
720
  message: "asins must not contain the same ASIN twice"
670
721
  });
671
- var inputSchema12 = {
672
- marketplace: z11.enum(SUPPORTED_MARKETPLACES).describe(
722
+ var inputSchema13 = {
723
+ marketplace: z12.enum(SUPPORTED_MARKETPLACES).describe(
673
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`)."
674
725
  ),
675
726
  asins: normalizedAsins.describe(
676
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.`
677
728
  ),
678
- confirm: z11.boolean().optional().describe(
729
+ confirm: z12.boolean().optional().describe(
679
730
  "Must be true to proceed \u2014 this dive spends dive tokens for every ASIN. Confirm the cost with the user first."
680
731
  )
681
732
  };
@@ -683,7 +734,7 @@ var createNicheDiveFromCompetitorsListTool = {
683
734
  name: "create_niche_dive_from_competitors_list",
684
735
  title: "Create a Niche Dive from a Competitor List",
685
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\`.`,
686
- inputSchema: inputSchema12,
737
+ inputSchema: inputSchema13,
687
738
  // Additive rather than destructive, but flagged destructive so clients prompt before the
688
739
  // irreversible token spend — same reasoning as create_niche_dive. The confirm gate enforces it.
689
740
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
@@ -703,23 +754,23 @@ var createNicheDiveFromCompetitorsListTool = {
703
754
  };
704
755
 
705
756
  // src/tools/redive-niche.ts
706
- import { z as z12 } from "zod";
707
- var inputSchema13 = {
708
- nicheId: z12.string().min(1).describe("The niche to re-dive. Get one from `list_niches`."),
709
- 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(
710
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."
711
762
  ),
712
- numberOfCompetitors: z12.number().int().min(2).optional().describe(
763
+ numberOfCompetitors: z13.number().int().min(2).optional().describe(
713
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."
714
765
  ),
715
- heroAsin: z12.string().min(1).optional().describe(
766
+ heroAsin: z13.string().min(1).optional().describe(
716
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."
717
768
  ),
718
- lockedAsins: z12.array(z12.string().min(1)).optional().describe(
769
+ lockedAsins: z13.array(z13.string().min(1)).optional().describe(
719
770
  "`discover` mode only. ASINs that must stay in the competitor set regardless of what discovery finds. Cannot exceed `numberOfCompetitors`, and cannot overlap `excludedAsins`."
720
771
  ),
721
- excludedAsins: z12.array(z12.string().min(1)).optional().describe("`discover` mode only. ASINs discovery must never select."),
722
- 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(
723
774
  "Must be true to proceed \u2014 a re-dive spends dive tokens. Confirm the cost with the user first."
724
775
  )
725
776
  };
@@ -727,7 +778,7 @@ var rediveNicheTool = {
727
778
  name: "redive_niche",
728
779
  title: "Re-dive an Existing Niche",
729
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.",
730
- inputSchema: inputSchema13,
781
+ inputSchema: inputSchema14,
731
782
  // Same reasoning as create_niche_dive: a re-dive is additive rather than destructive, but the
732
783
  // irreversible token spend is what clients need to prompt on. The confirm gate enforces it regardless.
733
784
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
@@ -760,17 +811,17 @@ var rediveNicheTool = {
760
811
  };
761
812
 
762
813
  // src/tools/get-dive-status.ts
763
- import { z as z13 } from "zod";
764
- var inputSchema14 = {
765
- diveId: z13.string().min(1).describe(
766
- "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`."
767
818
  )
768
819
  };
769
820
  var getDiveStatusTool = {
770
821
  name: "get_dive_status",
771
822
  title: "Get Niche Dive Status",
772
- 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.",
773
- 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,
774
825
  annotations: { readOnlyHint: true },
775
826
  handler: async (args, ctx) => {
776
827
  return await httpGet(
@@ -780,11 +831,166 @@ var getDiveStatusTool = {
780
831
  }
781
832
  };
782
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
+
783
989
  // src/tools/delete-niche.ts
784
- import { z as z14 } from "zod";
785
- var inputSchema15 = {
786
- nicheId: z14.string().min(1).describe("The niche to delete. Get one from `list_niches`."),
787
- 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(
788
994
  "Must be true to proceed \u2014 deleting a niche destroys its keywords, competitors and dive history permanently. Confirm with the user first."
789
995
  )
790
996
  };
@@ -792,7 +998,7 @@ var deleteNicheTool = {
792
998
  name: "delete_niche",
793
999
  title: "Delete a Niche",
794
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).",
795
- inputSchema: inputSchema15,
1001
+ inputSchema: inputSchema20,
796
1002
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
797
1003
  handler: async (args, ctx) => {
798
1004
  const pending = requireConfirmation(
@@ -810,15 +1016,15 @@ var deleteNicheTool = {
810
1016
  };
811
1017
 
812
1018
  // src/tools/pause-rank-radar.ts
813
- import { z as z15 } from "zod";
814
- var inputSchema16 = {
815
- 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`).")
816
1022
  };
817
1023
  var pauseRankRadarTool = {
818
1024
  name: "pause_rank_radar",
819
1025
  title: "Pause a Rank Radar",
820
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.",
821
- inputSchema: inputSchema16,
1027
+ inputSchema: inputSchema21,
822
1028
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
823
1029
  handler: async (args, ctx) => {
824
1030
  await httpPost(
@@ -831,15 +1037,15 @@ var pauseRankRadarTool = {
831
1037
  };
832
1038
 
833
1039
  // src/tools/resume-rank-radar.ts
834
- import { z as z16 } from "zod";
835
- var inputSchema17 = {
836
- 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`).")
837
1043
  };
838
1044
  var resumeRankRadarTool = {
839
1045
  name: "resume_rank_radar",
840
1046
  title: "Resume a Paused Rank Radar",
841
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.",
842
- inputSchema: inputSchema17,
1048
+ inputSchema: inputSchema22,
843
1049
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
844
1050
  handler: async (args, ctx) => {
845
1051
  await httpPost(
@@ -852,10 +1058,10 @@ var resumeRankRadarTool = {
852
1058
  };
853
1059
 
854
1060
  // src/tools/delete-rank-radar.ts
855
- import { z as z17 } from "zod";
856
- var inputSchema18 = {
857
- rankRadarId: z17.string().uuid().describe("The Rank Radar UUID to delete (from `list_rank_radars`)."),
858
- 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(
859
1065
  "Must be true to proceed \u2014 deleting a Rank Radar destroys its ranking history permanently. Confirm with the user first."
860
1066
  )
861
1067
  };
@@ -863,7 +1069,7 @@ var deleteRankRadarTool = {
863
1069
  name: "delete_rank_radar",
864
1070
  title: "Delete a Rank Radar",
865
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`.",
866
- inputSchema: inputSchema18,
1072
+ inputSchema: inputSchema23,
867
1073
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
868
1074
  handler: async (args, ctx) => {
869
1075
  const pending = requireConfirmation(
@@ -881,10 +1087,10 @@ var deleteRankRadarTool = {
881
1087
  };
882
1088
 
883
1089
  // src/tools/add-rank-radar-search-terms.ts
884
- import { z as z18 } from "zod";
885
- var inputSchema19 = {
886
- rankRadarId: z18.string().uuid().describe("The Rank Radar UUID to add the search terms to (from `list_rank_radars`)."),
887
- 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(
888
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.`
889
1095
  )
890
1096
  };
@@ -892,7 +1098,7 @@ var addRankRadarSearchTermsTool = {
892
1098
  name: "add_rank_radar_search_terms",
893
1099
  title: "Add Search Terms to a Rank Radar",
894
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.",
895
- inputSchema: inputSchema19,
1101
+ inputSchema: inputSchema24,
896
1102
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
897
1103
  handler: async (args, ctx) => {
898
1104
  return await httpPost(
@@ -904,10 +1110,10 @@ var addRankRadarSearchTermsTool = {
904
1110
  };
905
1111
 
906
1112
  // src/tools/pause-rank-radar-search-terms.ts
907
- import { z as z19 } from "zod";
908
- var inputSchema20 = {
909
- rankRadarId: z19.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
910
- 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(
911
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."
912
1118
  )
913
1119
  };
@@ -915,7 +1121,7 @@ var pauseRankRadarSearchTermsTool = {
915
1121
  name: "pause_rank_radar_search_terms",
916
1122
  title: "Pause Search Terms of a Rank Radar",
917
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.",
918
- inputSchema: inputSchema20,
1124
+ inputSchema: inputSchema25,
919
1125
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
920
1126
  handler: async (args, ctx) => {
921
1127
  await httpPost(
@@ -932,10 +1138,10 @@ var pauseRankRadarSearchTermsTool = {
932
1138
  };
933
1139
 
934
1140
  // src/tools/resume-rank-radar-search-terms.ts
935
- import { z as z20 } from "zod";
936
- var inputSchema21 = {
937
- rankRadarId: z20.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
938
- 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(
939
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."
940
1146
  )
941
1147
  };
@@ -943,7 +1149,7 @@ var resumeRankRadarSearchTermsTool = {
943
1149
  name: "resume_rank_radar_search_terms",
944
1150
  title: "Resume Paused Search Terms of a Rank Radar",
945
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.",
946
- inputSchema: inputSchema21,
1152
+ inputSchema: inputSchema26,
947
1153
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
948
1154
  handler: async (args, ctx) => {
949
1155
  await httpPost(
@@ -960,16 +1166,16 @@ var resumeRankRadarSearchTermsTool = {
960
1166
  };
961
1167
 
962
1168
  // src/tools/generate-listing-copy.ts
963
- import { z as z21 } from "zod";
964
- var inputSchema22 = {
965
- nicheId: z21.string().min(1).describe("The niche whose keyword data the copy is written from. Get one from `list_niches`."),
966
- 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(
967
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."
968
1174
  ),
969
- 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(
970
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.'
971
1177
  ),
972
- confirm: z21.boolean().optional().describe(
1178
+ confirm: z26.boolean().optional().describe(
973
1179
  "Must be true to proceed \u2014 each generation spends one AI Copywriter prompt from the quota. Confirm the cost with the user first."
974
1180
  )
975
1181
  };
@@ -977,7 +1183,7 @@ var generateListingCopyTool = {
977
1183
  name: "generate_listing_copy",
978
1184
  title: "Generate Listing Copy for a Niche",
979
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`.",
980
- inputSchema: inputSchema22,
1186
+ inputSchema: inputSchema27,
981
1187
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
982
1188
  handler: async (args, ctx) => {
983
1189
  const pending = requireConfirmation(
@@ -995,16 +1201,16 @@ var generateListingCopyTool = {
995
1201
  };
996
1202
 
997
1203
  // src/tools/get-listing-copy-generation-status.ts
998
- import { z as z22 } from "zod";
999
- var inputSchema23 = {
1000
- nicheId: z22.string().min(1).describe("The niche the generation was started for."),
1001
- 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`.")
1002
1208
  };
1003
1209
  var getListingCopyGenerationStatusTool = {
1004
1210
  name: "get_listing_copy_generation_status",
1005
1211
  title: "Get Listing Copy Generation Status",
1006
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`.",
1007
- inputSchema: inputSchema23,
1213
+ inputSchema: inputSchema28,
1008
1214
  annotations: { readOnlyHint: true },
1009
1215
  handler: async (args, ctx) => {
1010
1216
  return await httpGet(
@@ -1015,16 +1221,16 @@ var getListingCopyGenerationStatusTool = {
1015
1221
  };
1016
1222
 
1017
1223
  // src/tools/list-seller-profiles.ts
1018
- import { z as z23 } from "zod";
1019
- var inputSchema24 = {
1020
- currentPage: z23.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1021
- 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.")
1022
1228
  };
1023
1229
  var listSellerProfilesTool = {
1024
1230
  name: "list_seller_profiles",
1025
1231
  title: "List Connected Amazon Seller Profiles",
1026
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).',
1027
- inputSchema: inputSchema24,
1233
+ inputSchema: inputSchema29,
1028
1234
  annotations: { readOnlyHint: true },
1029
1235
  handler: async (args, ctx) => {
1030
1236
  return await httpGet(
@@ -1039,15 +1245,15 @@ var listSellerProfilesTool = {
1039
1245
  };
1040
1246
 
1041
1247
  // src/tools/get-seller-catalog.ts
1042
- import { z as z25 } from "zod";
1248
+ import { z as z30 } from "zod";
1043
1249
 
1044
1250
  // src/tools/seller-scoped.ts
1045
- import { z as z24 } from "zod";
1251
+ import { z as z29 } from "zod";
1046
1252
  var sellerScopedInputSchema = {
1047
- sellerId: z24.string().min(1).describe(
1253
+ sellerId: z29.string().min(1).describe(
1048
1254
  "Amazon seller account ID. Get it from `list_seller_profiles`, or find it on the Connections page at https://2.datadive.tools."
1049
1255
  ),
1050
- 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.')
1051
1257
  };
1052
1258
  function sellerMarketplacePath(sellerId, marketplace, suffix) {
1053
1259
  return `/v1/seller_profiles/${encodeURIComponent(sellerId)}/marketplaces/${encodeURIComponent(
@@ -1056,19 +1262,19 @@ function sellerMarketplacePath(sellerId, marketplace, suffix) {
1056
1262
  }
1057
1263
 
1058
1264
  // src/tools/get-seller-catalog.ts
1059
- var inputSchema25 = {
1265
+ var inputSchema30 = {
1060
1266
  ...sellerScopedInputSchema,
1061
- search: z25.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1062
- brand: z25.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1063
- status: z25.enum(CATALOG_STATUSES).optional().describe('Listing status filter. "Active" (default) or "all".'),
1064
- currentPage: z25.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1065
- 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.")
1066
1272
  };
1067
1273
  var getSellerCatalogTool = {
1068
1274
  name: "get_seller_catalog",
1069
1275
  title: "List Catalog ASINs for a Seller",
1070
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).",
1071
- inputSchema: inputSchema25,
1277
+ inputSchema: inputSchema30,
1072
1278
  annotations: { readOnlyHint: true },
1073
1279
  handler: async (args, ctx) => {
1074
1280
  return await httpGet(
@@ -1086,28 +1292,28 @@ var getSellerCatalogTool = {
1086
1292
  };
1087
1293
 
1088
1294
  // src/tools/get-seller-listing-changes.ts
1089
- import { z as z26 } from "zod";
1090
- var inputSchema26 = {
1295
+ import { z as z31 } from "zod";
1296
+ var inputSchema31 = {
1091
1297
  ...sellerScopedInputSchema,
1092
- 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".'),
1093
- asin: z26.string().min(1).optional().describe("Filter to an exact ASIN or parent ASIN."),
1094
- brand: z26.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1095
- search: z26.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1096
- 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."),
1097
- 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."),
1098
- sortBy: z26.enum(LISTING_CHANGE_SORT_BY).optional().describe('Sort field. "date" (default) or "type".'),
1099
- sortOrder: z26.enum(SORT_ORDER).optional().describe('Sort direction. "DESC" (default) or "ASC".'),
1100
- 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(
1101
1307
  "When true, include the ranking/conversion `correlation` object per change (CVR and search-term movement before vs after). Defaults to false."
1102
1308
  ),
1103
- currentPage: z26.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1104
- 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.")
1105
1311
  };
1106
1312
  var getSellerListingChangesTool = {
1107
1313
  name: "get_seller_listing_changes",
1108
1314
  title: "List Listing Changes for a Seller",
1109
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).",
1110
- inputSchema: inputSchema26,
1316
+ inputSchema: inputSchema31,
1111
1317
  annotations: { readOnlyHint: true },
1112
1318
  handler: async (args, ctx) => {
1113
1319
  return await httpGet(
@@ -1132,40 +1338,40 @@ var getSellerListingChangesTool = {
1132
1338
  };
1133
1339
 
1134
1340
  // src/tools/list-ppc-campaigns.ts
1135
- import { z as z27 } from "zod";
1341
+ import { z as z32 } from "zod";
1136
1342
  var PPC_CAMPAIGNS_MAX_WINDOW_DAYS = 90;
1137
1343
  var PPC_CAMPAIGNS_MAX_PAGE_SIZE = 50;
1138
- var inputSchema27 = {
1344
+ var inputSchema32 = {
1139
1345
  ...sellerScopedInputSchema,
1140
- startDate: z27.string().regex(
1346
+ startDate: z32.string().regex(
1141
1347
  ISO_8601,
1142
1348
  "startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)"
1143
1349
  ).optional().describe("Start of the reporting window, ISO-8601. Defaults to 30 days before endDate."),
1144
- endDate: z27.string().regex(
1350
+ endDate: z32.string().regex(
1145
1351
  ISO_8601,
1146
1352
  "endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)"
1147
1353
  ).optional().describe(
1148
1354
  `End of the reporting window, ISO-8601. Defaults to now. At most ${PPC_CAMPAIGNS_MAX_WINDOW_DAYS} days after startDate.`
1149
1355
  ),
1150
- asin: z27.string().min(1).optional().describe("Only campaigns advertising this ASIN. Cannot be combined with parentAsin."),
1151
- parentAsin: z27.string().min(1).optional().describe("Only campaigns advertising any ASIN of this variation family. Cannot be combined with asin."),
1152
- search: z27.string().min(3).max(200).optional().describe("Partial match on campaign name or advertised ASIN. At least 3 characters."),
1153
- 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(
1154
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."
1155
1361
  ),
1156
- 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(
1157
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.`
1158
1364
  ),
1159
- sortOrder: z27.enum(SORT_ORDER).optional().describe('Sort direction. "ASC" (default) or "DESC".'),
1160
- state: z27.enum(PPC_CAMPAIGN_STATES).optional().describe("Only ENABLED or only PAUSED campaigns. Omit for both."),
1161
- currentPage: z27.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1162
- 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.`)
1163
1369
  };
1164
1370
  var listPpcCampaignsTool = {
1165
1371
  name: "list_ppc_campaigns",
1166
1372
  title: "List PPC Campaigns for a Seller",
1167
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\`.`,
1168
- inputSchema: inputSchema27,
1374
+ inputSchema: inputSchema32,
1169
1375
  annotations: { readOnlyHint: true },
1170
1376
  handler: async (args, ctx) => {
1171
1377
  if (args.asin && args.parentAsin) {
@@ -1196,13 +1402,13 @@ var listPpcCampaignsTool = {
1196
1402
  };
1197
1403
 
1198
1404
  // src/tools/get-asin-inventory-distribution.ts
1199
- import { z as z28 } from "zod";
1200
- var inputSchema28 = {
1201
- sellerId: z28.string().min(1).describe(
1405
+ import { z as z33 } from "zod";
1406
+ var inputSchema33 = {
1407
+ sellerId: z33.string().min(1).describe(
1202
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."
1203
1409
  ),
1204
- marketplace: z28.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
1205
- 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(
1206
1412
  "Amazon Standard Identification Number (10 uppercase alphanumeric characters, e.g. B08XYZ1234)."
1207
1413
  )
1208
1414
  };
@@ -1210,7 +1416,7 @@ var getAsinInventoryDistributionTool = {
1210
1416
  name: "get_asin_inventory_distribution",
1211
1417
  title: "Get Per-Fulfillment-Center Inventory Distribution for an ASIN",
1212
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.",
1213
- inputSchema: inputSchema28,
1419
+ inputSchema: inputSchema33,
1214
1420
  annotations: { readOnlyHint: true },
1215
1421
  handler: async (args, ctx) => {
1216
1422
  return await httpGet(
@@ -1223,18 +1429,18 @@ var getAsinInventoryDistributionTool = {
1223
1429
  };
1224
1430
 
1225
1431
  // src/tools/alert-query.ts
1226
- import { z as z29 } from "zod";
1432
+ import { z as z34 } from "zod";
1227
1433
  var alertsQueryInputSchema = {
1228
- sellerId: z29.string().min(1).max(50).optional().describe('Filter to a single connected Amazon seller account ID, e.g. "A1B2C3D4E5".'),
1229
- 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.'),
1230
- 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(
1231
1437
  "Lifecycle filter. `active` (default) returns unresolved alerts, `resolved` returns resolved alerts, `all` returns both. Dismissed alerts are never returned."
1232
1438
  ),
1233
- 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(
1234
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."
1235
1441
  ),
1236
- currentPage: z29.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1237
- 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.")
1238
1444
  };
1239
1445
 
1240
1446
  // src/tools/list-indexing-issue-alerts.ts
@@ -1284,12 +1490,12 @@ var listBlindSpendAlertsTool = {
1284
1490
  };
1285
1491
 
1286
1492
  // src/tools/get-quota.ts
1287
- var inputSchema29 = {};
1493
+ var inputSchema34 = {};
1288
1494
  var getQuotaTool = {
1289
1495
  name: "get_quota",
1290
1496
  title: "Get Quota Usage",
1291
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.",
1292
- inputSchema: inputSchema29,
1498
+ inputSchema: inputSchema34,
1293
1499
  annotations: { readOnlyHint: true },
1294
1500
  handler: async (_args, ctx) => {
1295
1501
  return await httpGet({ config: ctx.config, toolName: "get_quota" }, "/v1/quota");
@@ -1297,20 +1503,20 @@ var getQuotaTool = {
1297
1503
  };
1298
1504
 
1299
1505
  // src/tools/list-usage.ts
1300
- import { z as z30 } from "zod";
1301
- var inputSchema30 = {
1302
- type: z30.enum(BILLABLE_FEATURE_TYPES).optional().describe("Filter to a single billable feature type. Omit to return all types."),
1303
- search: z30.string().min(1).optional().describe("Case-insensitive partial match on the user's name or email."),
1304
- 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."),
1305
- 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."),
1306
- currentPage: z30.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1307
- 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.")
1308
1514
  };
1309
1515
  var listUsageTool = {
1310
1516
  name: "list_usage",
1311
1517
  title: "List Billable Usage Logs",
1312
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).",
1313
- inputSchema: inputSchema30,
1519
+ inputSchema: inputSchema35,
1314
1520
  annotations: { readOnlyHint: true },
1315
1521
  handler: async (args, ctx) => {
1316
1522
  return await httpGet(
@@ -1332,6 +1538,7 @@ var listUsageTool = {
1332
1538
  var allTools = [
1333
1539
  listNichesTool,
1334
1540
  getNicheKeywordsTool,
1541
+ listKeywordExclusionsTool,
1335
1542
  getNicheRootsTool,
1336
1543
  getNicheCompetitorsTool,
1337
1544
  getRankingJuiceTool,
@@ -1344,6 +1551,10 @@ var allTools = [
1344
1551
  createNicheDiveFromCompetitorsListTool,
1345
1552
  rediveNicheTool,
1346
1553
  getDiveStatusTool,
1554
+ addNicheKeywordsTool,
1555
+ removeNicheKeywordsTool,
1556
+ addKeywordExclusionsTool,
1557
+ removeKeywordExclusionsTool,
1347
1558
  addRankRadarSearchTermsTool,
1348
1559
  pauseRankRadarSearchTermsTool,
1349
1560
  resumeRankRadarSearchTermsTool,
@@ -1366,6 +1577,21 @@ var allTools = [
1366
1577
 
1367
1578
  // src/whats-new.ts
1368
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
+ },
1588
+ {
1589
+ version: "0.16.1",
1590
+ released: "2026-09-29",
1591
+ items: [
1592
+ "`get_rank_radar_data` returns every tracked keyword of a Rank Radar in one answer again, also in AI apps that still show its older inputs. Pass `currentPage` / `pageSize` to read one page at a time."
1593
+ ]
1594
+ },
1369
1595
  {
1370
1596
  version: "0.16.0",
1371
1597
  released: "2026-09-28",
@@ -1403,8 +1629,8 @@ var WHATS_NEW = [
1403
1629
  }
1404
1630
  ];
1405
1631
  var INSTRUCTIONS_RELEASES = 2;
1406
- function formatEntries(entries) {
1407
- return entries.map((e) => `${e.released} (v${e.version}):
1632
+ function formatEntries(entries2) {
1633
+ return entries2.map((e) => `${e.released} (v${e.version}):
1408
1634
  ${e.items.map((i) => `- ${i}`).join("\n")}`).join("\n\n");
1409
1635
  }
1410
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.