@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/index.js CHANGED
@@ -144,7 +144,7 @@ function extractMessage(body) {
144
144
  }
145
145
 
146
146
  // package.json
147
- var version = "0.16.1";
147
+ var version = "0.17.0";
148
148
 
149
149
  // src/http/client.ts
150
150
  var PKG_VERSION = version;
@@ -321,7 +321,7 @@ var inputSchema2 = {
321
321
  var getNicheKeywordsTool = {
322
322
  name: "get_niche_keywords",
323
323
  title: "Get Master Keyword List for a Niche",
324
- 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.',
324
+ 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.',
325
325
  inputSchema: inputSchema2,
326
326
  annotations: { readOnlyHint: true },
327
327
  handler: async (args, ctx) => {
@@ -332,16 +332,35 @@ var getNicheKeywordsTool = {
332
332
  }
333
333
  };
334
334
 
335
- // src/tools/get-niche-roots.ts
335
+ // src/tools/list-keyword-exclusions.ts
336
336
  import { z as z3 } from "zod";
337
337
  var inputSchema3 = {
338
338
  nicheId: z3.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
339
339
  };
340
+ var listKeywordExclusionsTool = {
341
+ name: "list_keyword_exclusions",
342
+ title: "List a Niche's Keyword Exclusions",
343
+ 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`.",
344
+ inputSchema: inputSchema3,
345
+ annotations: { readOnlyHint: true },
346
+ handler: async (args, ctx) => {
347
+ return await httpGet(
348
+ { config: ctx.config, toolName: "list_keyword_exclusions" },
349
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keyword-exclusions`
350
+ );
351
+ }
352
+ };
353
+
354
+ // src/tools/get-niche-roots.ts
355
+ import { z as z4 } from "zod";
356
+ var inputSchema4 = {
357
+ nicheId: z4.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
358
+ };
340
359
  var getNicheRootsTool = {
341
360
  name: "get_niche_roots",
342
361
  title: "Get Keyword Roots for a Niche",
343
362
  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.",
344
- inputSchema: inputSchema3,
363
+ inputSchema: inputSchema4,
345
364
  annotations: { readOnlyHint: true },
346
365
  handler: async (args, ctx) => {
347
366
  return await httpGet(
@@ -352,15 +371,15 @@ var getNicheRootsTool = {
352
371
  };
353
372
 
354
373
  // src/tools/get-niche-competitors.ts
355
- import { z as z4 } from "zod";
356
- var inputSchema4 = {
357
- nicheId: z4.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
374
+ import { z as z5 } from "zod";
375
+ var inputSchema5 = {
376
+ nicheId: z5.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
358
377
  };
359
378
  var getNicheCompetitorsTool = {
360
379
  name: "get_niche_competitors",
361
380
  title: "Get Competitors and Niche Statistics",
362
381
  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`.",
363
- inputSchema: inputSchema4,
382
+ inputSchema: inputSchema5,
364
383
  annotations: { readOnlyHint: true },
365
384
  handler: async (args, ctx) => {
366
385
  return await httpGet(
@@ -371,15 +390,15 @@ var getNicheCompetitorsTool = {
371
390
  };
372
391
 
373
392
  // src/tools/get-ranking-juice.ts
374
- import { z as z5 } from "zod";
375
- var inputSchema5 = {
376
- nicheId: z5.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
393
+ import { z as z6 } from "zod";
394
+ var inputSchema6 = {
395
+ nicheId: z6.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
377
396
  };
378
397
  var getRankingJuiceTool = {
379
398
  name: "get_ranking_juice",
380
399
  title: "Get Ranking Juices for a Niche",
381
400
  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.",
382
- inputSchema: inputSchema5,
401
+ inputSchema: inputSchema6,
383
402
  annotations: { readOnlyHint: true },
384
403
  handler: async (args, ctx) => {
385
404
  return await httpGet(
@@ -390,7 +409,7 @@ var getRankingJuiceTool = {
390
409
  };
391
410
 
392
411
  // src/tools/list-rank-radars.ts
393
- import { z as z6 } from "zod";
412
+ import { z as z7 } from "zod";
394
413
 
395
414
  // src/types/api.ts
396
415
  var ISO_8601 = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
@@ -406,6 +425,10 @@ var SUPPORTED_MARKETPLACES = [
406
425
  "it",
407
426
  "co.jp"
408
427
  ];
428
+ var NICHE_KEYWORDS_MAX_PER_CALL = 100;
429
+ var NICHE_KEYWORD_MAX_LENGTH = 255;
430
+ var KEYWORD_EXCLUSIONS_MAX_PER_CALL = 100;
431
+ var KEYWORD_EXCLUSIONS_MAX_PER_NICHE = 1e3;
409
432
  var RANK_RADAR_STATES = ["ACTIVE", "PAUSED", "ARCHIVED"];
410
433
  var RANK_RADAR_STATUSES = [...RANK_RADAR_STATES, "ALL"];
411
434
  var PPC_CAMPAIGN_BASE_SORT_BY = [
@@ -451,14 +474,14 @@ var LISTING_COPY_STRATEGIES = ["cosmo", "ranking-juice", "nlp", "cosmo-rufus"];
451
474
 
452
475
  // src/tools/list-rank-radars.ts
453
476
  var LIST_RANK_RADARS_MAX_PAGE_SIZE = 50;
454
- var inputSchema6 = {
455
- currentPage: z6.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
456
- 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.`),
457
- nicheId: z6.string().min(1).optional().describe("Filter Rank Radars by Niche identifier. Use after `list_niches`."),
458
- status: z6.enum(RANK_RADAR_STATUSES).optional().describe(
477
+ var inputSchema7 = {
478
+ currentPage: z7.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
479
+ 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.`),
480
+ nicheId: z7.string().min(1).optional().describe("Filter Rank Radars by Niche identifier. Use after `list_niches`."),
481
+ status: z7.enum(RANK_RADAR_STATUSES).optional().describe(
459
482
  "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."
460
483
  ),
461
- searchText: z6.string().optional().describe("Filter Rank Radars by ASIN or product title.")
484
+ searchText: z7.string().optional().describe("Filter Rank Radars by ASIN or product title.")
462
485
  };
463
486
  function minimize(item) {
464
487
  const { asin, ...rest } = item;
@@ -468,7 +491,7 @@ var listRankRadarsTool = {
468
491
  name: "list_rank_radars",
469
492
  title: "List Rank Radars",
470
493
  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.`,
471
- inputSchema: inputSchema6,
494
+ inputSchema: inputSchema7,
472
495
  annotations: { readOnlyHint: true },
473
496
  handler: async (args, ctx) => {
474
497
  const page = await httpGet(
@@ -487,18 +510,18 @@ var listRankRadarsTool = {
487
510
  };
488
511
 
489
512
  // src/tools/rank-radar-keyword-query.ts
490
- import { z as z7 } from "zod";
513
+ import { z as z8 } from "zod";
491
514
  var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
492
515
  var RANK_RADAR_KEYWORDS_MAX_PAGE_SIZE = 100;
493
516
  var RANK_RADAR_MAX_DATE_RANGE_DAYS = 90;
494
517
  var rankRadarKeywordQueryInputSchema = {
495
- rankRadarId: z7.string().uuid().describe("The Rank Radar UUID (from `list_rank_radars`)."),
496
- 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)."),
497
- endDate: z7.string().regex(ISO_DATE, "endDate must be in yyyy-mm-dd format").describe(
518
+ rankRadarId: z8.string().uuid().describe("The Rank Radar UUID (from `list_rank_radars`)."),
519
+ 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)."),
520
+ endDate: z8.string().regex(ISO_DATE, "endDate must be in yyyy-mm-dd format").describe(
498
521
  `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.`
499
522
  ),
500
- currentPage: z7.number().int().min(1).optional().describe("Page of keywords, 1-indexed. Defaults to 1."),
501
- 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.`)
523
+ currentPage: z8.number().int().min(1).optional().describe("Page of keywords, 1-indexed. Defaults to 1."),
524
+ 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.`)
502
525
  };
503
526
  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.`;
504
527
  function rankRadarPath(rankRadarId, suffix = "") {
@@ -514,7 +537,7 @@ function rankRadarKeywordQuery(args) {
514
537
  }
515
538
 
516
539
  // src/tools/get-rank-radar-data.ts
517
- var inputSchema7 = rankRadarKeywordQueryInputSchema;
540
+ var inputSchema8 = rankRadarKeywordQueryInputSchema;
518
541
  async function readAllKeywords(args, ctx) {
519
542
  const request = { config: ctx.config, toolName: "get_rank_radar_data" };
520
543
  const path = rankRadarPath(args.rankRadarId);
@@ -544,7 +567,7 @@ var getRankRadarDataTool = {
544
567
  name: "get_rank_radar_data",
545
568
  title: "Get Keyword Rankings for a Rank Radar",
546
569
  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\`.`,
547
- inputSchema: inputSchema7,
570
+ inputSchema: inputSchema8,
548
571
  annotations: { readOnlyHint: true },
549
572
  handler: async (args, ctx) => {
550
573
  if (args.currentPage === void 0 && args.pageSize === void 0) {
@@ -559,12 +582,12 @@ var getRankRadarDataTool = {
559
582
  };
560
583
 
561
584
  // src/tools/get-rank-radar-sqp-data.ts
562
- var inputSchema8 = rankRadarKeywordQueryInputSchema;
585
+ var inputSchema9 = rankRadarKeywordQueryInputSchema;
563
586
  var getRankRadarSqpDataTool = {
564
587
  name: "get_rank_radar_sqp_data",
565
588
  title: "Get Search Query Performance for a Rank Radar",
566
589
  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\`.`,
567
- inputSchema: inputSchema8,
590
+ inputSchema: inputSchema9,
568
591
  annotations: { readOnlyHint: true },
569
592
  handler: async (args, ctx) => {
570
593
  return await httpGet(
@@ -576,10 +599,10 @@ var getRankRadarSqpDataTool = {
576
599
  };
577
600
 
578
601
  // src/tools/get-rank-radar-ppc-data.ts
579
- import { z as z8 } from "zod";
580
- var inputSchema9 = {
602
+ import { z as z9 } from "zod";
603
+ var inputSchema10 = {
581
604
  ...rankRadarKeywordQueryInputSchema,
582
- includeCampaigns: z8.boolean().optional().describe(
605
+ includeCampaigns: z9.boolean().optional().describe(
583
606
  "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."
584
607
  )
585
608
  };
@@ -587,7 +610,7 @@ var getRankRadarPpcDataTool = {
587
610
  name: "get_rank_radar_ppc_data",
588
611
  title: "Get PPC Data for a Rank Radar",
589
612
  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\`.`,
590
- inputSchema: inputSchema9,
613
+ inputSchema: inputSchema10,
591
614
  annotations: { readOnlyHint: true },
592
615
  handler: async (args, ctx) => {
593
616
  return await httpGet(
@@ -599,7 +622,7 @@ var getRankRadarPpcDataTool = {
599
622
  };
600
623
 
601
624
  // src/tools/create-rank-radar.ts
602
- import { z as z9 } from "zod";
625
+ import { z as z10 } from "zod";
603
626
 
604
627
  // src/tools/confirm.ts
605
628
  function needsConfirmation(confirm, ctx) {
@@ -620,11 +643,11 @@ function requireConfirmation(confirm, ctx, costNote) {
620
643
 
621
644
  // src/tools/create-rank-radar.ts
622
645
  var PATH = "/v1/niches/rank-radars";
623
- var inputSchema10 = {
624
- asin: z9.string().min(1).describe("The ASIN to track keyword rankings for."),
625
- numberOfKeywords: z9.number().int().min(1).describe("How many keywords to track (minimum 1). More keywords = broader tracking but more Search Term tokens."),
626
- nicheId: z9.string().min(1).describe("The niche this Rank Radar belongs to. Get one from `list_niches`."),
627
- confirm: z9.boolean().optional().describe(
646
+ var inputSchema11 = {
647
+ asin: z10.string().min(1).describe("The ASIN to track keyword rankings for."),
648
+ numberOfKeywords: z10.number().int().min(1).describe("How many keywords to track (minimum 1). More keywords = broader tracking but more Search Term tokens."),
649
+ nicheId: z10.string().min(1).describe("The niche this Rank Radar belongs to. Get one from `list_niches`."),
650
+ confirm: z10.boolean().optional().describe(
628
651
  "Must be true to proceed \u2014 creating a Rank Radar spends Search Term tokens. Confirm the cost with the user first."
629
652
  )
630
653
  };
@@ -632,7 +655,7 @@ var createRankRadarTool = {
632
655
  name: "create_rank_radar",
633
656
  title: "Create a Rank Radar",
634
657
  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`.",
635
- inputSchema: inputSchema10,
658
+ inputSchema: inputSchema11,
636
659
  // destructiveHint is technically "additive" here (this creates a new Rank Radar, it
637
660
  // doesn't destroy data), but we flag it destructive so clients prompt before the
638
661
  // irreversible token spend. The confirm gate enforces this server-side regardless.
@@ -657,20 +680,20 @@ var createRankRadarTool = {
657
680
  };
658
681
 
659
682
  // src/tools/create-niche-dive.ts
660
- import { z as z10 } from "zod";
661
- var inputSchema11 = {
662
- marketplace: z10.enum(SUPPORTED_MARKETPLACES).describe(
683
+ import { z as z11 } from "zod";
684
+ var inputSchema12 = {
685
+ marketplace: z11.enum(SUPPORTED_MARKETPLACES).describe(
663
686
  "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`)."
664
687
  ),
665
- asin: z10.string().min(1).describe("The seed ASIN to start the dive from (e.g. B08N5WRWNW). Competitors are built around it."),
666
- numberOfCompetitors: z10.number().int().min(2).describe("How many competitors to analyze (minimum 2). More competitors = deeper niche but more dive tokens."),
667
- confirm: z10.boolean().optional().describe("Must be true to proceed \u2014 creating a dive spends dive tokens. Confirm the cost with the user first.")
688
+ asin: z11.string().min(1).describe("The seed ASIN to start the dive from (e.g. B08N5WRWNW). Competitors are built around it."),
689
+ numberOfCompetitors: z11.number().int().min(2).describe("How many competitors to analyze (minimum 2). More competitors = deeper niche but more dive tokens."),
690
+ confirm: z11.boolean().optional().describe("Must be true to proceed \u2014 creating a dive spends dive tokens. Confirm the cost with the user first.")
668
691
  };
669
692
  var createNicheDiveTool = {
670
693
  name: "create_niche_dive",
671
694
  title: "Create a Niche Dive",
672
695
  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).",
673
- inputSchema: inputSchema11,
696
+ inputSchema: inputSchema12,
674
697
  // destructiveHint is technically "additive" here (a dive creates a new niche, it
675
698
  // doesn't destroy data), but we flag it destructive so clients prompt before the
676
699
  // irreversible token spend. The confirm gate enforces this server-side regardless.
@@ -695,20 +718,20 @@ var createNicheDiveTool = {
695
718
  };
696
719
 
697
720
  // src/tools/create-niche-dive-from-competitors-list.ts
698
- import { z as z11 } from "zod";
721
+ import { z as z12 } from "zod";
699
722
  var DIVE_FROM_COMPETITORS_MIN_ASINS = 2;
700
723
  var DIVE_FROM_COMPETITORS_MAX_ASINS = 200;
701
- 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, {
724
+ 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, {
702
725
  message: "asins must not contain the same ASIN twice"
703
726
  });
704
- var inputSchema12 = {
705
- marketplace: z11.enum(SUPPORTED_MARKETPLACES).describe(
727
+ var inputSchema13 = {
728
+ marketplace: z12.enum(SUPPORTED_MARKETPLACES).describe(
706
729
  "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`)."
707
730
  ),
708
731
  asins: normalizedAsins.describe(
709
732
  `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.`
710
733
  ),
711
- confirm: z11.boolean().optional().describe(
734
+ confirm: z12.boolean().optional().describe(
712
735
  "Must be true to proceed \u2014 this dive spends dive tokens for every ASIN. Confirm the cost with the user first."
713
736
  )
714
737
  };
@@ -716,7 +739,7 @@ var createNicheDiveFromCompetitorsListTool = {
716
739
  name: "create_niche_dive_from_competitors_list",
717
740
  title: "Create a Niche Dive from a Competitor List",
718
741
  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\`.`,
719
- inputSchema: inputSchema12,
742
+ inputSchema: inputSchema13,
720
743
  // Additive rather than destructive, but flagged destructive so clients prompt before the
721
744
  // irreversible token spend — same reasoning as create_niche_dive. The confirm gate enforces it.
722
745
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
@@ -736,23 +759,23 @@ var createNicheDiveFromCompetitorsListTool = {
736
759
  };
737
760
 
738
761
  // src/tools/redive-niche.ts
739
- import { z as z12 } from "zod";
740
- var inputSchema13 = {
741
- nicheId: z12.string().min(1).describe("The niche to re-dive. Get one from `list_niches`."),
742
- mode: z12.enum(["same_competitors", "discover"]).describe(
762
+ import { z as z13 } from "zod";
763
+ var inputSchema14 = {
764
+ nicheId: z13.string().min(1).describe("The niche to re-dive. Get one from `list_niches`."),
765
+ mode: z13.enum(["same_competitors", "discover"]).describe(
743
766
  "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."
744
767
  ),
745
- numberOfCompetitors: z12.number().int().min(2).optional().describe(
768
+ numberOfCompetitors: z13.number().int().min(2).optional().describe(
746
769
  "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."
747
770
  ),
748
- heroAsin: z12.string().min(1).optional().describe(
771
+ heroAsin: z13.string().min(1).optional().describe(
749
772
  "`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."
750
773
  ),
751
- lockedAsins: z12.array(z12.string().min(1)).optional().describe(
774
+ lockedAsins: z13.array(z13.string().min(1)).optional().describe(
752
775
  "`discover` mode only. ASINs that must stay in the competitor set regardless of what discovery finds. Cannot exceed `numberOfCompetitors`, and cannot overlap `excludedAsins`."
753
776
  ),
754
- excludedAsins: z12.array(z12.string().min(1)).optional().describe("`discover` mode only. ASINs discovery must never select."),
755
- confirm: z12.boolean().optional().describe(
777
+ excludedAsins: z13.array(z13.string().min(1)).optional().describe("`discover` mode only. ASINs discovery must never select."),
778
+ confirm: z13.boolean().optional().describe(
756
779
  "Must be true to proceed \u2014 a re-dive spends dive tokens. Confirm the cost with the user first."
757
780
  )
758
781
  };
@@ -760,7 +783,7 @@ var rediveNicheTool = {
760
783
  name: "redive_niche",
761
784
  title: "Re-dive an Existing Niche",
762
785
  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.",
763
- inputSchema: inputSchema13,
786
+ inputSchema: inputSchema14,
764
787
  // Same reasoning as create_niche_dive: a re-dive is additive rather than destructive, but the
765
788
  // irreversible token spend is what clients need to prompt on. The confirm gate enforces it regardless.
766
789
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
@@ -793,17 +816,17 @@ var rediveNicheTool = {
793
816
  };
794
817
 
795
818
  // src/tools/get-dive-status.ts
796
- import { z as z13 } from "zod";
797
- var inputSchema14 = {
798
- diveId: z13.string().min(1).describe(
799
- "The dive identifier returned by `create_niche_dive`, `create_niche_dive_from_competitors_list` or `redive_niche`."
819
+ import { z as z14 } from "zod";
820
+ var inputSchema15 = {
821
+ diveId: z14.string().min(1).describe(
822
+ "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`."
800
823
  )
801
824
  };
802
825
  var getDiveStatusTool = {
803
826
  name: "get_dive_status",
804
827
  title: "Get Niche Dive Status",
805
- 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.",
806
- inputSchema: inputSchema14,
828
+ 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.",
829
+ inputSchema: inputSchema15,
807
830
  annotations: { readOnlyHint: true },
808
831
  handler: async (args, ctx) => {
809
832
  return await httpGet(
@@ -813,11 +836,166 @@ var getDiveStatusTool = {
813
836
  }
814
837
  };
815
838
 
839
+ // src/tools/add-niche-keywords.ts
840
+ import { z as z16 } from "zod";
841
+
842
+ // src/tools/niche-keyword-list.ts
843
+ import { z as z15 } from "zod";
844
+ var nicheKeyword = () => z15.string().trim().toLowerCase().min(1).max(NICHE_KEYWORD_MAX_LENGTH);
845
+ function uniqueKeywords(schema, field) {
846
+ return schema.refine((keywords) => new Set(keywords).size === keywords.length, {
847
+ message: `${field} must not contain the same keyword twice (compared lower-cased and trimmed)`
848
+ });
849
+ }
850
+ var nicheKeywordList = uniqueKeywords(
851
+ z15.array(nicheKeyword()).min(1).max(NICHE_KEYWORDS_MAX_PER_CALL),
852
+ "keywords"
853
+ );
854
+
855
+ // src/tools/add-niche-keywords.ts
856
+ var inputSchema16 = {
857
+ nicheId: z16.string().min(1).describe("The niche whose Master Keyword List to add to. Get one from `list_niches`."),
858
+ keywords: nicheKeywordList.describe(
859
+ `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.`
860
+ ),
861
+ confirm: z16.boolean().optional().describe(
862
+ "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."
863
+ ),
864
+ maxTokensUsed: z16.number().int().min(0).optional().describe(
865
+ "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."
866
+ )
867
+ };
868
+ function isMaxTokensExceededBody(body) {
869
+ if (!body || typeof body !== "object") return false;
870
+ const b = body;
871
+ return b.error === "MAX_TOKENS_EXCEEDED" && typeof b.tokensUsed === "number";
872
+ }
873
+ var addNicheKeywordsTool = {
874
+ name: "add_niche_keywords",
875
+ title: "Add Keywords to a Niche's Master Keyword List",
876
+ 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.",
877
+ inputSchema: inputSchema16,
878
+ // Like redive_niche: additive, but it can spend Dive tokens irreversibly, which is what clients
879
+ // need to prompt on. The confirm gate covers the paid path regardless.
880
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
881
+ handler: async (args, ctx) => {
882
+ const request = { config: ctx.config, toolName: "add_niche_keywords" };
883
+ const path = `/v1/niches/${encodeURIComponent(args.nicheId)}/keywords`;
884
+ if (needsConfirmation(args.confirm, ctx)) {
885
+ try {
886
+ return await httpPost(request, path, {
887
+ keywords: args.keywords,
888
+ maxTokensUsed: 0
889
+ });
890
+ } catch (err) {
891
+ if (err instanceof ApiError && isMaxTokensExceededBody(err.body)) {
892
+ const tokenCost = err.body.tokensUsed;
893
+ return {
894
+ ...confirmationRequired(
895
+ `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.`
896
+ ),
897
+ tokenCost
898
+ };
899
+ }
900
+ throw err;
901
+ }
902
+ }
903
+ return await httpPost(request, path, {
904
+ keywords: args.keywords,
905
+ maxTokensUsed: args.maxTokensUsed
906
+ });
907
+ }
908
+ };
909
+
910
+ // src/tools/remove-niche-keywords.ts
911
+ import { z as z17 } from "zod";
912
+ var inputSchema17 = {
913
+ nicheId: z17.string().min(1).describe("The niche whose Master Keyword List to remove from. Get one from `list_niches`."),
914
+ keywords: nicheKeywordList.describe(
915
+ `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.`
916
+ )
917
+ };
918
+ var removeNicheKeywordsTool = {
919
+ name: "remove_niche_keywords",
920
+ title: "Remove Keywords from a Niche's Master Keyword List",
921
+ 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.",
922
+ inputSchema: inputSchema17,
923
+ // Deletes hand-added keywords, and putting them back can cost Dive tokens on a stale niche.
924
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
925
+ handler: async (args, ctx) => {
926
+ return await httpPost(
927
+ { config: ctx.config, toolName: "remove_niche_keywords" },
928
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keywords/remove`,
929
+ { keywords: args.keywords }
930
+ );
931
+ }
932
+ };
933
+
934
+ // src/tools/keyword-exclusions-input.ts
935
+ import { z as z18 } from "zod";
936
+ var entries = (field) => uniqueKeywords(z18.array(nicheKeyword()).max(KEYWORD_EXCLUSIONS_MAX_PER_CALL), field).optional();
937
+ var keywordExclusionsInputSchema = (verb) => ({
938
+ nicheId: z18.string().min(1).describe("The niche whose keyword exclusions to change. Get one from `list_niches`."),
939
+ exact: entries("exact").describe(
940
+ `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.`
941
+ ),
942
+ phrase: entries("phrase").describe(
943
+ `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.`
944
+ )
945
+ });
946
+ function assertExclusionCount(exact, phrase) {
947
+ const total = (exact?.length ?? 0) + (phrase?.length ?? 0);
948
+ if (total === 0 || total > KEYWORD_EXCLUSIONS_MAX_PER_CALL) {
949
+ throw new ApiError(
950
+ "bad_request",
951
+ 400,
952
+ `Send 1 to ${KEYWORD_EXCLUSIONS_MAX_PER_CALL} entries in total across \`exact\` and \`phrase\` (got ${total}). Split a longer list over several calls.`
953
+ );
954
+ }
955
+ }
956
+
957
+ // src/tools/add-keyword-exclusions.ts
958
+ var inputSchema18 = keywordExclusionsInputSchema("exclude");
959
+ var addKeywordExclusionsTool = {
960
+ name: "add_keyword_exclusions",
961
+ title: "Exclude Keywords from a Niche",
962
+ 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.`,
963
+ inputSchema: inputSchema18,
964
+ // Hides data from every MKL-based view until undone; reversible, but clients should prompt on it.
965
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
966
+ handler: async (args, ctx) => {
967
+ assertExclusionCount(args.exact, args.phrase);
968
+ return await httpPost(
969
+ { config: ctx.config, toolName: "add_keyword_exclusions" },
970
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keyword-exclusions`,
971
+ { exact: args.exact, phrase: args.phrase }
972
+ );
973
+ }
974
+ };
975
+
976
+ // src/tools/remove-keyword-exclusions.ts
977
+ var inputSchema19 = keywordExclusionsInputSchema("stop excluding");
978
+ var removeKeywordExclusionsTool = {
979
+ name: "remove_keyword_exclusions",
980
+ title: "Remove Keyword Exclusions from a Niche",
981
+ 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.`,
982
+ inputSchema: inputSchema19,
983
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
984
+ handler: async (args, ctx) => {
985
+ assertExclusionCount(args.exact, args.phrase);
986
+ return await httpPost(
987
+ { config: ctx.config, toolName: "remove_keyword_exclusions" },
988
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keyword-exclusions/remove`,
989
+ { exact: args.exact, phrase: args.phrase }
990
+ );
991
+ }
992
+ };
993
+
816
994
  // src/tools/delete-niche.ts
817
- import { z as z14 } from "zod";
818
- var inputSchema15 = {
819
- nicheId: z14.string().min(1).describe("The niche to delete. Get one from `list_niches`."),
820
- confirm: z14.boolean().optional().describe(
995
+ import { z as z19 } from "zod";
996
+ var inputSchema20 = {
997
+ nicheId: z19.string().min(1).describe("The niche to delete. Get one from `list_niches`."),
998
+ confirm: z19.boolean().optional().describe(
821
999
  "Must be true to proceed \u2014 deleting a niche destroys its keywords, competitors and dive history permanently. Confirm with the user first."
822
1000
  )
823
1001
  };
@@ -825,7 +1003,7 @@ var deleteNicheTool = {
825
1003
  name: "delete_niche",
826
1004
  title: "Delete a Niche",
827
1005
  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).",
828
- inputSchema: inputSchema15,
1006
+ inputSchema: inputSchema20,
829
1007
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
830
1008
  handler: async (args, ctx) => {
831
1009
  const pending = requireConfirmation(
@@ -843,15 +1021,15 @@ var deleteNicheTool = {
843
1021
  };
844
1022
 
845
1023
  // src/tools/pause-rank-radar.ts
846
- import { z as z15 } from "zod";
847
- var inputSchema16 = {
848
- rankRadarId: z15.string().uuid().describe("The Rank Radar UUID to pause (from `list_rank_radars`).")
1024
+ import { z as z20 } from "zod";
1025
+ var inputSchema21 = {
1026
+ rankRadarId: z20.string().uuid().describe("The Rank Radar UUID to pause (from `list_rank_radars`).")
849
1027
  };
850
1028
  var pauseRankRadarTool = {
851
1029
  name: "pause_rank_radar",
852
1030
  title: "Pause a Rank Radar",
853
1031
  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.",
854
- inputSchema: inputSchema16,
1032
+ inputSchema: inputSchema21,
855
1033
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
856
1034
  handler: async (args, ctx) => {
857
1035
  await httpPost(
@@ -864,15 +1042,15 @@ var pauseRankRadarTool = {
864
1042
  };
865
1043
 
866
1044
  // src/tools/resume-rank-radar.ts
867
- import { z as z16 } from "zod";
868
- var inputSchema17 = {
869
- rankRadarId: z16.string().uuid().describe("The Rank Radar UUID to resume (from `list_rank_radars`).")
1045
+ import { z as z21 } from "zod";
1046
+ var inputSchema22 = {
1047
+ rankRadarId: z21.string().uuid().describe("The Rank Radar UUID to resume (from `list_rank_radars`).")
870
1048
  };
871
1049
  var resumeRankRadarTool = {
872
1050
  name: "resume_rank_radar",
873
1051
  title: "Resume a Paused Rank Radar",
874
1052
  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.",
875
- inputSchema: inputSchema17,
1053
+ inputSchema: inputSchema22,
876
1054
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
877
1055
  handler: async (args, ctx) => {
878
1056
  await httpPost(
@@ -885,10 +1063,10 @@ var resumeRankRadarTool = {
885
1063
  };
886
1064
 
887
1065
  // src/tools/delete-rank-radar.ts
888
- import { z as z17 } from "zod";
889
- var inputSchema18 = {
890
- rankRadarId: z17.string().uuid().describe("The Rank Radar UUID to delete (from `list_rank_radars`)."),
891
- confirm: z17.boolean().optional().describe(
1066
+ import { z as z22 } from "zod";
1067
+ var inputSchema23 = {
1068
+ rankRadarId: z22.string().uuid().describe("The Rank Radar UUID to delete (from `list_rank_radars`)."),
1069
+ confirm: z22.boolean().optional().describe(
892
1070
  "Must be true to proceed \u2014 deleting a Rank Radar destroys its ranking history permanently. Confirm with the user first."
893
1071
  )
894
1072
  };
@@ -896,7 +1074,7 @@ var deleteRankRadarTool = {
896
1074
  name: "delete_rank_radar",
897
1075
  title: "Delete a Rank Radar",
898
1076
  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`.",
899
- inputSchema: inputSchema18,
1077
+ inputSchema: inputSchema23,
900
1078
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
901
1079
  handler: async (args, ctx) => {
902
1080
  const pending = requireConfirmation(
@@ -914,10 +1092,10 @@ var deleteRankRadarTool = {
914
1092
  };
915
1093
 
916
1094
  // src/tools/add-rank-radar-search-terms.ts
917
- import { z as z18 } from "zod";
918
- var inputSchema19 = {
919
- rankRadarId: z18.string().uuid().describe("The Rank Radar UUID to add the search terms to (from `list_rank_radars`)."),
920
- searchTerms: z18.array(z18.string().min(1)).min(1).max(ADD_SEARCH_TERMS_MAX).describe(
1095
+ import { z as z23 } from "zod";
1096
+ var inputSchema24 = {
1097
+ rankRadarId: z23.string().uuid().describe("The Rank Radar UUID to add the search terms to (from `list_rank_radars`)."),
1098
+ searchTerms: z23.array(z23.string().min(1)).min(1).max(ADD_SEARCH_TERMS_MAX).describe(
921
1099
  `The keywords to start tracking, 1\u2013${ADD_SEARCH_TERMS_MAX} per call. Must be unique. Amazon-ignored special characters (@, #, _, parentheses, \u2026) are rejected.`
922
1100
  )
923
1101
  };
@@ -925,7 +1103,7 @@ var addRankRadarSearchTermsTool = {
925
1103
  name: "add_rank_radar_search_terms",
926
1104
  title: "Add Search Terms to a Rank Radar",
927
1105
  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.",
928
- inputSchema: inputSchema19,
1106
+ inputSchema: inputSchema24,
929
1107
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
930
1108
  handler: async (args, ctx) => {
931
1109
  return await httpPost(
@@ -937,10 +1115,10 @@ var addRankRadarSearchTermsTool = {
937
1115
  };
938
1116
 
939
1117
  // src/tools/pause-rank-radar-search-terms.ts
940
- import { z as z19 } from "zod";
941
- var inputSchema20 = {
942
- rankRadarId: z19.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
943
- rankRadarKeywordIds: z19.array(z19.string().uuid()).min(1).describe(
1118
+ import { z as z24 } from "zod";
1119
+ var inputSchema25 = {
1120
+ rankRadarId: z24.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
1121
+ rankRadarKeywordIds: z24.array(z24.string().uuid()).min(1).describe(
944
1122
  "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."
945
1123
  )
946
1124
  };
@@ -948,7 +1126,7 @@ var pauseRankRadarSearchTermsTool = {
948
1126
  name: "pause_rank_radar_search_terms",
949
1127
  title: "Pause Search Terms of a Rank Radar",
950
1128
  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.",
951
- inputSchema: inputSchema20,
1129
+ inputSchema: inputSchema25,
952
1130
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
953
1131
  handler: async (args, ctx) => {
954
1132
  await httpPost(
@@ -965,10 +1143,10 @@ var pauseRankRadarSearchTermsTool = {
965
1143
  };
966
1144
 
967
1145
  // src/tools/resume-rank-radar-search-terms.ts
968
- import { z as z20 } from "zod";
969
- var inputSchema21 = {
970
- rankRadarId: z20.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
971
- rankRadarKeywordIds: z20.array(z20.string().uuid()).min(1).describe(
1146
+ import { z as z25 } from "zod";
1147
+ var inputSchema26 = {
1148
+ rankRadarId: z25.string().uuid().describe("The Rank Radar UUID the search terms belong to."),
1149
+ rankRadarKeywordIds: z25.array(z25.string().uuid()).min(1).describe(
972
1150
  "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."
973
1151
  )
974
1152
  };
@@ -976,7 +1154,7 @@ var resumeRankRadarSearchTermsTool = {
976
1154
  name: "resume_rank_radar_search_terms",
977
1155
  title: "Resume Paused Search Terms of a Rank Radar",
978
1156
  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.",
979
- inputSchema: inputSchema21,
1157
+ inputSchema: inputSchema26,
980
1158
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
981
1159
  handler: async (args, ctx) => {
982
1160
  await httpPost(
@@ -993,16 +1171,16 @@ var resumeRankRadarSearchTermsTool = {
993
1171
  };
994
1172
 
995
1173
  // src/tools/generate-listing-copy.ts
996
- import { z as z21 } from "zod";
997
- var inputSchema22 = {
998
- nicheId: z21.string().min(1).describe("The niche whose keyword data the copy is written from. Get one from `list_niches`."),
999
- strategy: z21.enum(LISTING_COPY_STRATEGIES).describe(
1174
+ import { z as z26 } from "zod";
1175
+ var inputSchema27 = {
1176
+ nicheId: z26.string().min(1).describe("The niche whose keyword data the copy is written from. Get one from `list_niches`."),
1177
+ strategy: z26.enum(LISTING_COPY_STRATEGIES).describe(
1000
1178
  "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."
1001
1179
  ),
1002
- currentListing: z21.record(z21.union([z21.string(), z21.array(z21.string())])).describe(
1180
+ currentListing: z26.record(z26.union([z26.string(), z26.array(z26.string())])).describe(
1003
1181
  '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.'
1004
1182
  ),
1005
- confirm: z21.boolean().optional().describe(
1183
+ confirm: z26.boolean().optional().describe(
1006
1184
  "Must be true to proceed \u2014 each generation spends one AI Copywriter prompt from the quota. Confirm the cost with the user first."
1007
1185
  )
1008
1186
  };
@@ -1010,7 +1188,7 @@ var generateListingCopyTool = {
1010
1188
  name: "generate_listing_copy",
1011
1189
  title: "Generate Listing Copy for a Niche",
1012
1190
  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`.",
1013
- inputSchema: inputSchema22,
1191
+ inputSchema: inputSchema27,
1014
1192
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
1015
1193
  handler: async (args, ctx) => {
1016
1194
  const pending = requireConfirmation(
@@ -1028,16 +1206,16 @@ var generateListingCopyTool = {
1028
1206
  };
1029
1207
 
1030
1208
  // src/tools/get-listing-copy-generation-status.ts
1031
- import { z as z22 } from "zod";
1032
- var inputSchema23 = {
1033
- nicheId: z22.string().min(1).describe("The niche the generation was started for."),
1034
- generationId: z22.string().min(1).describe("The `generationId` returned by `generate_listing_copy`.")
1209
+ import { z as z27 } from "zod";
1210
+ var inputSchema28 = {
1211
+ nicheId: z27.string().min(1).describe("The niche the generation was started for."),
1212
+ generationId: z27.string().min(1).describe("The `generationId` returned by `generate_listing_copy`.")
1035
1213
  };
1036
1214
  var getListingCopyGenerationStatusTool = {
1037
1215
  name: "get_listing_copy_generation_status",
1038
1216
  title: "Get Listing Copy Generation Status",
1039
1217
  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`.",
1040
- inputSchema: inputSchema23,
1218
+ inputSchema: inputSchema28,
1041
1219
  annotations: { readOnlyHint: true },
1042
1220
  handler: async (args, ctx) => {
1043
1221
  return await httpGet(
@@ -1048,16 +1226,16 @@ var getListingCopyGenerationStatusTool = {
1048
1226
  };
1049
1227
 
1050
1228
  // src/tools/list-seller-profiles.ts
1051
- import { z as z23 } from "zod";
1052
- var inputSchema24 = {
1053
- currentPage: z23.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1054
- pageSize: z23.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1229
+ import { z as z28 } from "zod";
1230
+ var inputSchema29 = {
1231
+ currentPage: z28.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1232
+ pageSize: z28.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1055
1233
  };
1056
1234
  var listSellerProfilesTool = {
1057
1235
  name: "list_seller_profiles",
1058
1236
  title: "List Connected Amazon Seller Profiles",
1059
1237
  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).',
1060
- inputSchema: inputSchema24,
1238
+ inputSchema: inputSchema29,
1061
1239
  annotations: { readOnlyHint: true },
1062
1240
  handler: async (args, ctx) => {
1063
1241
  return await httpGet(
@@ -1072,15 +1250,15 @@ var listSellerProfilesTool = {
1072
1250
  };
1073
1251
 
1074
1252
  // src/tools/get-seller-catalog.ts
1075
- import { z as z25 } from "zod";
1253
+ import { z as z30 } from "zod";
1076
1254
 
1077
1255
  // src/tools/seller-scoped.ts
1078
- import { z as z24 } from "zod";
1256
+ import { z as z29 } from "zod";
1079
1257
  var sellerScopedInputSchema = {
1080
- sellerId: z24.string().min(1).describe(
1258
+ sellerId: z29.string().min(1).describe(
1081
1259
  "Amazon seller account ID. Get it from `list_seller_profiles`, or find it on the Connections page at https://2.datadive.tools."
1082
1260
  ),
1083
- marketplace: z24.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.')
1261
+ marketplace: z29.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.')
1084
1262
  };
1085
1263
  function sellerMarketplacePath(sellerId, marketplace, suffix) {
1086
1264
  return `/v1/seller_profiles/${encodeURIComponent(sellerId)}/marketplaces/${encodeURIComponent(
@@ -1089,19 +1267,19 @@ function sellerMarketplacePath(sellerId, marketplace, suffix) {
1089
1267
  }
1090
1268
 
1091
1269
  // src/tools/get-seller-catalog.ts
1092
- var inputSchema25 = {
1270
+ var inputSchema30 = {
1093
1271
  ...sellerScopedInputSchema,
1094
- search: z25.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1095
- brand: z25.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1096
- status: z25.enum(CATALOG_STATUSES).optional().describe('Listing status filter. "Active" (default) or "all".'),
1097
- currentPage: z25.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1098
- pageSize: z25.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1272
+ search: z30.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1273
+ brand: z30.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1274
+ status: z30.enum(CATALOG_STATUSES).optional().describe('Listing status filter. "Active" (default) or "all".'),
1275
+ currentPage: z30.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1276
+ pageSize: z30.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1099
1277
  };
1100
1278
  var getSellerCatalogTool = {
1101
1279
  name: "get_seller_catalog",
1102
1280
  title: "List Catalog ASINs for a Seller",
1103
1281
  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).",
1104
- inputSchema: inputSchema25,
1282
+ inputSchema: inputSchema30,
1105
1283
  annotations: { readOnlyHint: true },
1106
1284
  handler: async (args, ctx) => {
1107
1285
  return await httpGet(
@@ -1119,28 +1297,28 @@ var getSellerCatalogTool = {
1119
1297
  };
1120
1298
 
1121
1299
  // src/tools/get-seller-listing-changes.ts
1122
- import { z as z26 } from "zod";
1123
- var inputSchema26 = {
1300
+ import { z as z31 } from "zod";
1301
+ var inputSchema31 = {
1124
1302
  ...sellerScopedInputSchema,
1125
- 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".'),
1126
- asin: z26.string().min(1).optional().describe("Filter to an exact ASIN or parent ASIN."),
1127
- brand: z26.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1128
- search: z26.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1129
- 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."),
1130
- 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."),
1131
- sortBy: z26.enum(LISTING_CHANGE_SORT_BY).optional().describe('Sort field. "date" (default) or "type".'),
1132
- sortOrder: z26.enum(SORT_ORDER).optional().describe('Sort direction. "DESC" (default) or "ASC".'),
1133
- includeCorrelations: z26.boolean().optional().describe(
1303
+ 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".'),
1304
+ asin: z31.string().min(1).optional().describe("Filter to an exact ASIN or parent ASIN."),
1305
+ brand: z31.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
1306
+ search: z31.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
1307
+ 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."),
1308
+ 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."),
1309
+ sortBy: z31.enum(LISTING_CHANGE_SORT_BY).optional().describe('Sort field. "date" (default) or "type".'),
1310
+ sortOrder: z31.enum(SORT_ORDER).optional().describe('Sort direction. "DESC" (default) or "ASC".'),
1311
+ includeCorrelations: z31.boolean().optional().describe(
1134
1312
  "When true, include the ranking/conversion `correlation` object per change (CVR and search-term movement before vs after). Defaults to false."
1135
1313
  ),
1136
- currentPage: z26.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1137
- pageSize: z26.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1314
+ currentPage: z31.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1315
+ pageSize: z31.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1138
1316
  };
1139
1317
  var getSellerListingChangesTool = {
1140
1318
  name: "get_seller_listing_changes",
1141
1319
  title: "List Listing Changes for a Seller",
1142
1320
  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).",
1143
- inputSchema: inputSchema26,
1321
+ inputSchema: inputSchema31,
1144
1322
  annotations: { readOnlyHint: true },
1145
1323
  handler: async (args, ctx) => {
1146
1324
  return await httpGet(
@@ -1165,40 +1343,40 @@ var getSellerListingChangesTool = {
1165
1343
  };
1166
1344
 
1167
1345
  // src/tools/list-ppc-campaigns.ts
1168
- import { z as z27 } from "zod";
1346
+ import { z as z32 } from "zod";
1169
1347
  var PPC_CAMPAIGNS_MAX_WINDOW_DAYS = 90;
1170
1348
  var PPC_CAMPAIGNS_MAX_PAGE_SIZE = 50;
1171
- var inputSchema27 = {
1349
+ var inputSchema32 = {
1172
1350
  ...sellerScopedInputSchema,
1173
- startDate: z27.string().regex(
1351
+ startDate: z32.string().regex(
1174
1352
  ISO_8601,
1175
1353
  "startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)"
1176
1354
  ).optional().describe("Start of the reporting window, ISO-8601. Defaults to 30 days before endDate."),
1177
- endDate: z27.string().regex(
1355
+ endDate: z32.string().regex(
1178
1356
  ISO_8601,
1179
1357
  "endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)"
1180
1358
  ).optional().describe(
1181
1359
  `End of the reporting window, ISO-8601. Defaults to now. At most ${PPC_CAMPAIGNS_MAX_WINDOW_DAYS} days after startDate.`
1182
1360
  ),
1183
- asin: z27.string().min(1).optional().describe("Only campaigns advertising this ASIN. Cannot be combined with parentAsin."),
1184
- parentAsin: z27.string().min(1).optional().describe("Only campaigns advertising any ASIN of this variation family. Cannot be combined with asin."),
1185
- search: z27.string().min(3).max(200).optional().describe("Partial match on campaign name or advertised ASIN. At least 3 characters."),
1186
- includePlacements: z27.boolean().optional().describe(
1361
+ asin: z32.string().min(1).optional().describe("Only campaigns advertising this ASIN. Cannot be combined with parentAsin."),
1362
+ parentAsin: z32.string().min(1).optional().describe("Only campaigns advertising any ASIN of this variation family. Cannot be combined with asin."),
1363
+ search: z32.string().min(3).max(200).optional().describe("Partial match on campaign name or advertised ASIN. At least 3 characters."),
1364
+ includePlacements: z32.boolean().optional().describe(
1187
1365
  "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."
1188
1366
  ),
1189
- sortBy: z27.enum([...PPC_CAMPAIGN_BASE_SORT_BY, ...PPC_CAMPAIGN_PLACEMENT_SORT_BY]).optional().describe(
1367
+ sortBy: z32.enum([...PPC_CAMPAIGN_BASE_SORT_BY, ...PPC_CAMPAIGN_PLACEMENT_SORT_BY]).optional().describe(
1190
1368
  `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.`
1191
1369
  ),
1192
- sortOrder: z27.enum(SORT_ORDER).optional().describe('Sort direction. "ASC" (default) or "DESC".'),
1193
- state: z27.enum(PPC_CAMPAIGN_STATES).optional().describe("Only ENABLED or only PAUSED campaigns. Omit for both."),
1194
- currentPage: z27.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1195
- 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.`)
1370
+ sortOrder: z32.enum(SORT_ORDER).optional().describe('Sort direction. "ASC" (default) or "DESC".'),
1371
+ state: z32.enum(PPC_CAMPAIGN_STATES).optional().describe("Only ENABLED or only PAUSED campaigns. Omit for both."),
1372
+ currentPage: z32.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1373
+ 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.`)
1196
1374
  };
1197
1375
  var listPpcCampaignsTool = {
1198
1376
  name: "list_ppc_campaigns",
1199
1377
  title: "List PPC Campaigns for a Seller",
1200
1378
  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\`.`,
1201
- inputSchema: inputSchema27,
1379
+ inputSchema: inputSchema32,
1202
1380
  annotations: { readOnlyHint: true },
1203
1381
  handler: async (args, ctx) => {
1204
1382
  if (args.asin && args.parentAsin) {
@@ -1229,13 +1407,13 @@ var listPpcCampaignsTool = {
1229
1407
  };
1230
1408
 
1231
1409
  // src/tools/get-asin-inventory-distribution.ts
1232
- import { z as z28 } from "zod";
1233
- var inputSchema28 = {
1234
- sellerId: z28.string().min(1).describe(
1410
+ import { z as z33 } from "zod";
1411
+ var inputSchema33 = {
1412
+ sellerId: z33.string().min(1).describe(
1235
1413
  "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."
1236
1414
  ),
1237
- marketplace: z28.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
1238
- asin: z28.string().regex(/^[A-Z0-9]{10}$/, "ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).").describe(
1415
+ marketplace: z33.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
1416
+ asin: z33.string().regex(/^[A-Z0-9]{10}$/, "ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).").describe(
1239
1417
  "Amazon Standard Identification Number (10 uppercase alphanumeric characters, e.g. B08XYZ1234)."
1240
1418
  )
1241
1419
  };
@@ -1243,7 +1421,7 @@ var getAsinInventoryDistributionTool = {
1243
1421
  name: "get_asin_inventory_distribution",
1244
1422
  title: "Get Per-Fulfillment-Center Inventory Distribution for an ASIN",
1245
1423
  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.",
1246
- inputSchema: inputSchema28,
1424
+ inputSchema: inputSchema33,
1247
1425
  annotations: { readOnlyHint: true },
1248
1426
  handler: async (args, ctx) => {
1249
1427
  return await httpGet(
@@ -1256,18 +1434,18 @@ var getAsinInventoryDistributionTool = {
1256
1434
  };
1257
1435
 
1258
1436
  // src/tools/alert-query.ts
1259
- import { z as z29 } from "zod";
1437
+ import { z as z34 } from "zod";
1260
1438
  var alertsQueryInputSchema = {
1261
- sellerId: z29.string().min(1).max(50).optional().describe('Filter to a single connected Amazon seller account ID, e.g. "A1B2C3D4E5".'),
1262
- 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.'),
1263
- status: z29.enum(ALERT_STATUSES).optional().describe(
1439
+ sellerId: z34.string().min(1).max(50).optional().describe('Filter to a single connected Amazon seller account ID, e.g. "A1B2C3D4E5".'),
1440
+ 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.'),
1441
+ status: z34.enum(ALERT_STATUSES).optional().describe(
1264
1442
  "Lifecycle filter. `active` (default) returns unresolved alerts, `resolved` returns resolved alerts, `all` returns both. Dismissed alerts are never returned."
1265
1443
  ),
1266
- 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(
1444
+ 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(
1267
1445
  "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."
1268
1446
  ),
1269
- currentPage: z29.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1270
- pageSize: z29.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1447
+ currentPage: z34.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1448
+ pageSize: z34.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
1271
1449
  };
1272
1450
 
1273
1451
  // src/tools/list-indexing-issue-alerts.ts
@@ -1317,12 +1495,12 @@ var listBlindSpendAlertsTool = {
1317
1495
  };
1318
1496
 
1319
1497
  // src/tools/get-quota.ts
1320
- var inputSchema29 = {};
1498
+ var inputSchema34 = {};
1321
1499
  var getQuotaTool = {
1322
1500
  name: "get_quota",
1323
1501
  title: "Get Quota Usage",
1324
1502
  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.",
1325
- inputSchema: inputSchema29,
1503
+ inputSchema: inputSchema34,
1326
1504
  annotations: { readOnlyHint: true },
1327
1505
  handler: async (_args, ctx) => {
1328
1506
  return await httpGet({ config: ctx.config, toolName: "get_quota" }, "/v1/quota");
@@ -1330,20 +1508,20 @@ var getQuotaTool = {
1330
1508
  };
1331
1509
 
1332
1510
  // src/tools/list-usage.ts
1333
- import { z as z30 } from "zod";
1334
- var inputSchema30 = {
1335
- type: z30.enum(BILLABLE_FEATURE_TYPES).optional().describe("Filter to a single billable feature type. Omit to return all types."),
1336
- search: z30.string().min(1).optional().describe("Case-insensitive partial match on the user's name or email."),
1337
- 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."),
1338
- 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."),
1339
- currentPage: z30.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1340
- pageSize: z30.number().int().min(1).max(200).optional().describe("Items per page (max 200). Defaults to 50.")
1511
+ import { z as z35 } from "zod";
1512
+ var inputSchema35 = {
1513
+ type: z35.enum(BILLABLE_FEATURE_TYPES).optional().describe("Filter to a single billable feature type. Omit to return all types."),
1514
+ search: z35.string().min(1).optional().describe("Case-insensitive partial match on the user's name or email."),
1515
+ 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."),
1516
+ 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."),
1517
+ currentPage: z35.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
1518
+ pageSize: z35.number().int().min(1).max(200).optional().describe("Items per page (max 200). Defaults to 50.")
1341
1519
  };
1342
1520
  var listUsageTool = {
1343
1521
  name: "list_usage",
1344
1522
  title: "List Billable Usage Logs",
1345
1523
  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).",
1346
- inputSchema: inputSchema30,
1524
+ inputSchema: inputSchema35,
1347
1525
  annotations: { readOnlyHint: true },
1348
1526
  handler: async (args, ctx) => {
1349
1527
  return await httpGet(
@@ -1365,6 +1543,7 @@ var listUsageTool = {
1365
1543
  var allTools = [
1366
1544
  listNichesTool,
1367
1545
  getNicheKeywordsTool,
1546
+ listKeywordExclusionsTool,
1368
1547
  getNicheRootsTool,
1369
1548
  getNicheCompetitorsTool,
1370
1549
  getRankingJuiceTool,
@@ -1377,6 +1556,10 @@ var allTools = [
1377
1556
  createNicheDiveFromCompetitorsListTool,
1378
1557
  rediveNicheTool,
1379
1558
  getDiveStatusTool,
1559
+ addNicheKeywordsTool,
1560
+ removeNicheKeywordsTool,
1561
+ addKeywordExclusionsTool,
1562
+ removeKeywordExclusionsTool,
1380
1563
  addRankRadarSearchTermsTool,
1381
1564
  pauseRankRadarSearchTermsTool,
1382
1565
  resumeRankRadarSearchTermsTool,
@@ -1399,6 +1582,14 @@ var allTools = [
1399
1582
 
1400
1583
  // src/whats-new.ts
1401
1584
  var WHATS_NEW = [
1585
+ {
1586
+ version: "0.17.0",
1587
+ released: "2026-10-12",
1588
+ items: [
1589
+ "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.",
1590
+ "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."
1591
+ ]
1592
+ },
1402
1593
  {
1403
1594
  version: "0.16.1",
1404
1595
  released: "2026-09-29",
@@ -1443,8 +1634,8 @@ var WHATS_NEW = [
1443
1634
  }
1444
1635
  ];
1445
1636
  var INSTRUCTIONS_RELEASES = 2;
1446
- function formatEntries(entries) {
1447
- return entries.map((e) => `${e.released} (v${e.version}):
1637
+ function formatEntries(entries2) {
1638
+ return entries2.map((e) => `${e.released} (v${e.version}):
1448
1639
  ${e.items.map((i) => `- ${i}`).join("\n")}`).join("\n\n");
1449
1640
  }
1450
1641
  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.