
Linkfox Junglescout Keyword By Asin
- 234 installs
- 64 repo stars
- Updated August 3, 2026
- linkfox-ai/linkfox-skills
linkfox-junglescout-keyword-by-asin is an agent skill that queries Jungle Scout via LinkFox to reverse-lookup Amazon keywords from up to 10 ASINs for developers and sellers analyzing competitor traffic across 10 marketpl
About
linkfox-junglescout-keyword-by-asin is a LinkFox agent skill that calls the Jungle Scout data gateway to reverse-lookup keywords ranking for up to 10 Amazon ASINs across 10 marketplaces (us, uk, de, in, ca, fr, it, es, mx, jp). Each result row returns keyword name, monthly exact and broad search volume, organic and sponsored ranks, relevancy score, ease-of-ranking score, PPC exact and broad bids, SP brand ad bid, competitor rank arrays, and trend fields. Developers filter by min/max search volume, word count, organic product count, and sort by `-monthly_search_volume_exact` or `-relevancy_score`. A bundled Python script `scripts/junglescout_keyword_by_asin.py` executes queries; large responses persist via `scripts/response_io.py` to avoid context overflow. Reach for this skill when analyzing competitor ASIN traffic words, expanding keyword lists, or sizing PPC bids—not for keyword text search or ABA rank history.
- Reverse-lookups keywords for up to 10 ASINs per call via Jungle Scout API
- Covers 10 Amazon marketplaces: us, uk, de, in, ca, fr, it, es, mx, jp
- Returns search volume, organic/sponsored rank, relevancy, and PPC bid fields
- Includes scripts/junglescout_keyword_by_asin.py and response_io.py persistence
- Supports filters for min/max search volume, word count, and sort by relevancy
Linkfox Junglescout Keyword By Asin by the numbers
- 234 all-time installs (skills.sh)
- +35 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #925 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/linkfox-ai/linkfox-skills --skill linkfox-junglescout-keyword-by-asinAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 234 |
|---|---|
| repo stars | ★ 64 |
| Last updated | August 3, 2026 |
| Repository | linkfox-ai/linkfox-skills ↗ |
How do you reverse lookup Amazon keywords from ASINs?
Query linkfox-junglescout-keyword-by-asin with marketplace us and up to 10 competitor ASINs to export ranked keywords with search volume, organic rank, and PPC bid data.
Who is it for?
Amazon sellers and ecommerce developers who need Jungle Scout ASIN keyword reverse lookup with search volume, rank, and PPC bid metrics across 10 marketplaces.
Skip if: Non-Amazon SEO, keyword text research without ASINs, or teams without LinkFox API access to the Jungle Scout gateway.
When should I use this skill?
A user provides Amazon ASINs and asks for competitor keywords, traffic terms, reverse ASIN lookup, or PPC bid references for listing or ad optimization.
What you get
Keyword table with search volume, organic and sponsored ranks, relevancy scores, PPC bids, competitor rank arrays, and persisted JSON/CSV via response_io.py.
- keyword ranking table
- persisted JSON/CSV response files
- PPC bid reference data
By the numbers
- Supports up to 10 ASINs per query
- Covers 10 Amazon marketplaces
- Returns 20+ keyword metric fields including PPC exact and broad bids
Files
Jungle Scout — 根据 ASIN 反查关键词
This skill queries keywords associated with given ASINs via the Jungle Scout data source, returning keyword search volume, competition metrics, PPC bids, ranking positions, and relevancy scores across 10 Amazon marketplaces. It supports up to 10 ASINs per call.
Core Concepts
Jungle Scout ASIN 反查关键词工具通过输入竞品或目标 ASIN,获取这些 ASIN 在亚马逊搜索结果中出现的所有关键词及详细指标。卖家可以利用此工具进行:
- 竞品关键词分析:查看竞品在哪些关键词下获得自然/广告排名
- 关键词拓展:从已知 ASIN 反向挖掘高潜力关键词
- 广告投放参考:获取关键词的 PPC 出价(精确/广泛匹配)和 SP 品牌广告出价
- 竞争格局评估:通过 Ease of Ranking Score 和竞品排名数据判断关键词竞争难度
- 流量结构解析:了解 ASIN 的流量来自哪些关键词,各关键词的搜索量和排名如何
数据维度:每条记录代表一个关键词,包含搜索量、趋势、排名、竞价、竞争度等完整指标。
Data Fields
Output Fields
| Field | API Name | Description | Example |
|---|---|---|---|
| 关键词 | name | 搜索关键词 | yoga mat |
| 站点 | country | 市场代码 | us |
| 精确搜索量 | monthlySearchVolumeExact | 月精确匹配搜索量 | 85420 |
| 广泛搜索量 | monthlySearchVolumeBroad | 月广泛匹配搜索量 | 125000 |
| 月趋势 | monthlyTrend | 月环比趋势(%) | 15.5 |
| 季度趋势 | quarterlyTrend | 季度趋势(%) | 8.2 |
| 主类目 | dominantCategory | 关键词主要类目 | Sports & Outdoors |
| 相关度 | relevancyScore | 关键词与 ASIN 的相关度(0-100) | 92 |
| 排名难度 | easeOfRankingScore | 排名容易程度(0-100,越高越容易) | 45 |
| 自然排名 | organicRank | ASIN 的自然搜索排名 | 5 |
| 广告排名 | sponsoredRank | ASIN 的广告排名 | 3 |
| 综合排名 | overallRank | 综合排名位置 | 4 |
| 自然结果数 | organicProductCount | 自然搜索结果中的商品总数 | 2000 |
| 广告结果数 | sponsoredProductCount | 广告位商品总数 | 48 |
| PPC精确出价 | ppcBidExact | 精确匹配 PPC 建议出价(USD) | 1.25 |
| PPC广泛出价 | ppcBidBroad | 广泛匹配 PPC 建议出价(USD) | 0.95 |
| SP品牌广告出价 | spBrandAdBid | SP 品牌广告建议出价(USD) | 2.10 |
| 推荐促销数 | recommendedPromotions | 推荐促销量 | 5 |
| 主力ASIN | primaryAsin | 该关键词下排名最高的 ASIN | B0XXXXXXXX |
| 自然相对位置 | relativeOrganicPosition | 查询 ASIN 的自然排名相对位置 | 0.12 |
| 广告相对位置 | relativeSponsoredPosition | 查询 ASIN 的广告排名相对位置 | 0.08 |
| 自然排名ASIN数 | organicRankingAsinsCount | 有自然排名的查询 ASIN 数量 | 3 |
| 广告排名ASIN数 | sponsoredRankingAsinsCount | 有广告排名的查询 ASIN 数量 | 2 |
| 竞品平均自然排名 | avgCompetitorOrganicRank | 查询 ASIN 的平均自然排名 | 12.5 |
| 竞品平均广告排名 | avgCompetitorSponsoredRank | 查询 ASIN 的平均广告排名 | 8.3 |
| 变体最低自然排名 | variationLowestOrganicRank | 变体中最佳自然排名 | 3 |
| 变体最低广告排名 | variationLowestSponsoredRank | 变体中最佳广告排名 | 2 |
| 竞品自然排名详情 | competitorOrganicRank | 各 ASIN 的自然排名数组 | [{asin, organicRank}] |
| 竞品广告排名详情 | competitorSponsoredRank | 各 ASIN 的广告排名数组 | [{asin, sponsoredRank}] |
| 更新时间 | updatedAt | 数据最后更新时间 | 2026-04-10 |
| 消耗Token | costToken | 本次调用消耗的 token 数 | 10 |
Supported Marketplaces
us (United States), uk (United Kingdom), de (Germany), in (India), ca (Canada), fr (France), it (Italy), es (Spain), mx (Mexico), jp (Japan)
Default marketplace is us. Use us when the user doesn't specify a marketplace.
API Usage
This tool calls the LinkFox tool gateway API. See references/api.md for calling conventions, request parameters, and response structure. You can also execute scripts/junglescout_keyword_by_asin.py directly to run queries.
How to Build Queries
Required parameters: marketplace and asins. All other parameters are optional filters for narrowing results.
Principles for Building API Calls
1. 站点映射:用户说"美国站"→ us,"日本站"→ jp,"德国站"→ de;未指定时默认 us 2. ASIN 格式:标准 10 位亚马逊 ASIN(以 B0 开头),以数组传入,最多 10 个 3. 搜索量筛选:用户说"搜索量大于1万"→ minMonthlySearchVolumeExact: 10000;"搜索量1000到5000"→ min: 1000, max: 5000 4. 排序选择:默认按精确搜索量降序(-monthly_search_volume_exact);用户要求"按相关度排序"→ sort: -relevancy_score 5. 结果数量:用户说"给我前50个"→ needCount: 50;未指定时可根据场景适当设置(如 30-100) 6. 变体包含:用户关注变体流量时设 includeVariants: true
Common Query Scenarios
1. 查看竞品 ASIN 的核心流量词
{
"marketplace": "us",
"asins": ["B0XXXXXXXX"],
"needCount": 50,
"sort": "-monthly_search_volume_exact"
}2. 多个 ASIN 的共同关键词(竞品对比)
{
"marketplace": "us",
"asins": ["B0XXXXXXXX", "B0YYYYYYYY", "B0ZZZZZZZZ"],
"needCount": 100,
"sort": "-relevancy_score"
}3. 筛选高搜索量低竞争关键词
{
"marketplace": "us",
"asins": ["B0XXXXXXXX"],
"minMonthlySearchVolumeExact": 5000,
"maxOrganicProductCount": 500,
"needCount": 50,
"sort": "-ease_of_ranking_score"
}4. 查找长尾关键词(多词组合)
{
"marketplace": "us",
"asins": ["B0XXXXXXXX"],
"minWordCount": 3,
"minMonthlySearchVolumeExact": 500,
"needCount": 80,
"sort": "-monthly_search_volume_exact"
}5. 日本站竞品广告关键词分析
{
"marketplace": "jp",
"asins": ["B0XXXXXXXX"],
"needCount": 50,
"sort": "-ppc_bid_exact"
}6. 包含变体的全量关键词挖掘
{
"marketplace": "de",
"asins": ["B0XXXXXXXX"],
"includeVariants": true,
"needCount": 200,
"sort": "-monthly_search_volume_exact"
}Display Rules
1. 表格展示为主:以表格形式展示关键词列表,核心列包括:关键词、精确搜索量、自然排名、广告排名、相关度、PPC 出价 2. 按需精简列:根据用户意图选择展示列。竞品分析侧重排名和搜索量;广告分析侧重 PPC 出价和广告排名 3. 排名高亮:对自然排名前 10 和广告排名前 5 的关键词做标注,帮助用户快速识别核心流量词 4. 趋势标注:月/季度趋势为正时标注增长,为负时标注下降 5. 竞品对比:当输入多个 ASIN 时,展示各 ASIN 在关键词下的排名对比 6. Error handling: When a query fails, explain the reason based on the error response and suggest adjusting parameters
Important Limitations
- ASIN 数量上限:单次查询最多 10 个 ASIN
- 数据时效性:数据定期更新,
updatedAt字段标注最后更新时间 - 搜索量类型:同时提供精确匹配和广泛匹配两种搜索量
- 必填参数:
marketplace和asins缺一不可 - 排名数据:排名为该 ASIN 在对应关键词搜索结果中的位置,非实时数据
User Expression & Scenario Quick Reference
Applicable - 通过 ASIN 反查和拓展关键词:
| User Says | Scenario |
|---|---|
| "这个ASIN有哪些流量词" | 单个 ASIN 关键词反查 |
| "竞品用了哪些关键词" | 竞品关键词分析 |
| "帮我对比这几个ASIN的关键词" | 多 ASIN 关键词对比 |
| "这个产品搜什么词能搜到" | ASIN 反向搜索词查询 |
| "找一些搜索量大竞争小的词" | 高搜索量低竞争关键词筛选 |
| "这个ASIN的广告词有哪些" | ASIN 广告关键词分析 |
| "帮我拓展一下关键词" | 基于 ASIN 的关键词拓展 |
Not applicable - 超出 ASIN 反查关键词范围:
- 关键词搜索量历史趋势(需要关键词历史搜索量工具)
- ABA 搜索词排名(需要 ABA 工具)
- 商品销量估算、listing 优化建议
- 关键词广告投放执行(仅提供竞价参考数据)
- 非亚马逊平台的关键词数据
Boundary judgment: When users say "关键词", "流量词", or "搜索词", if they provide specific ASINs and want to know what keywords those ASINs rank for, this skill applies. If they want to search keywords by text or check historical search volume trends, other skills are more suitable.
Feedback:
Auto-detect and report feedback via the Feedback API when any of the following apply: 1. The functionality or purpose described in this skill does not match actual behavior 2. The skill's results do not match the user's intent 3. The user expresses dissatisfaction or praise about this skill 4. Anything you believe could be improved
Call the feedback API as specified in references/api.md. Do not interrupt the user's flow.
<!-- LF_LARGE_RESPONSE_BLOCK -->
Handling Large Responses
To avoid overflowing the agent context, persist the response to disk and extract only the fields you need:
python scripts/response_io.py run --script scripts/junglescout_keyword_by_asin.py --out-dir <DIR> '<params>'
python scripts/response_io.py read <file> --fields "<paths>" # or --path "<JMESPath>"Pick--out-diroutside any git working tree (e.g./tmp/...on Unix,%TEMP%/...on Windows). Persisted responses may contain PII, pricing, or auth-sensitive data — do not commit them. Files are not auto-deleted; clean up when the task is done.
run writes the full response to a file and emits only a schema preview + file path. read projects specific fields, with --limit/--offset for slicing and --format json|jsonl|csv|table for output.
When to prefer this pattern — apply your judgment based on the response characteristics, e.g.:
- High field count per record, or fields you don't need
- Batch/paginated results (multiple items per call)
- Long-text fields (descriptions, reviews, HTML, time series)
- Output reused across later steps rather than consumed immediately
For small, single-use responses, calling the main script directly is fine.
⚠️ The preview is a truncated schema + sample, not the full data. Any field-level decision must read from the persisted file via read. <!-- /LF_LARGE_RESPONSE_BLOCK -->
--- For more high-quality, professional cross-border e-commerce skills, visit [LinkFox Skills](https://skill.linkfox.com/).
Jungle Scout 根据 ASIN 反查关键词 API 参考
调用规范
- 请求地址:
https://tool-gateway.linkfox.com/tool-jungle-scout/keywords/by-asin - 请求方式:POST,Content-Type: application/json
- 认证方式:Header
Authorization: <api_key>,api_key 从环境变量LINKFOXAGENT_API_KEY读取(如未配置,提示用户前往 https://skill.linkfox.com/linkfoxskills/guide.htm 申请)
请求参数
POST Body(JSON):
必填参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| marketplace | string | 是 | 目标市场代码。可选值:us、uk、de、in、ca、fr、it、es、mx、jp |
| asins | array\<string\> | 是 | ASIN 列表,最多 10 个 |
可选参数 — 结果控制
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| needCount | int | 否 | 返回的结果总数(内部自动分页) |
| includeVariants | boolean | 否 | 是否包含变体商品的关键词 |
| sort | string | 否 | 排序字段,默认 -monthly_search_volume_exact(精确搜索量降序)。可选值见下方排序字段表 |
可选参数 — 搜索量筛选
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| minMonthlySearchVolumeExact | int | 否 | 最小月精确搜索量(1-999999) |
| maxMonthlySearchVolumeExact | int | 否 | 最大月精确搜索量(1-999999) |
| minMonthlySearchVolumeBroad | int | 否 | 最小月广泛搜索量(1-999999) |
| maxMonthlySearchVolumeBroad | int | 否 | 最大月广泛搜索量(1-999999) |
可选参数 — 关键词特征筛选
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| minWordCount | int | 否 | 最小单词数(1-99999) |
| maxWordCount | int | 否 | 最大单词数(1-99999) |
| minOrganicProductCount | int | 否 | 最小自然搜索结果数(1-99999) |
| maxOrganicProductCount | int | 否 | 最大自然搜索结果数(1-99999) |
排序字段
| sort 值 | 说明 |
|---|---|
| name / -name | 关键词名称 升序/降序 |
| dominant_category / -dominant_category | 主类目 升序/降序 |
| monthly_trend / -monthly_trend | 月趋势 升序/降序 |
| quarterly_trend / -quarterly_trend | 季度趋势 升序/降序 |
| monthly_search_volume_exact / -monthly_search_volume_exact | 精确搜索量 升序/降序(默认降序) |
| monthly_search_volume_broad / -monthly_search_volume_broad | 广泛搜索量 升序/降序 |
| recommended_promotions / -recommended_promotions | 推荐促销 升序/降序 |
| sp_brand_ad_bid / -sp_brand_ad_bid | SP品牌广告出价 升序/降序 |
| ppc_bid_broad / -ppc_bid_broad | PPC广泛出价 升序/降序 |
| ppc_bid_exact / -ppc_bid_exact | PPC精确出价 升序/降序 |
| ease_of_ranking_score / -ease_of_ranking_score | 排名难度分 升序/降序 |
| relevancy_score / -relevancy_score | 相关度分 升序/降序 |
| organic_product_count / -organic_product_count | 自然结果数 升序/降序 |
站点映射
| 站点 | marketplace 值 |
|---|---|
| 美国 | us |
| 英国 | uk |
| 德国 | de |
| 印度 | in |
| 加拿大 | ca |
| 法国 | fr |
| 意大利 | it |
| 西班牙 | es |
| 墨西哥 | mx |
| 日本 | jp |
响应结构
| 字段 | 类型 | 说明 |
|---|---|---|
| costToken | integer | 消耗 token 数 |
| keywordInfoList | array | 关键词信息列表 |
keywordInfoList 数组中每个对象
| 字段 | 类型 | 说明 |
|---|---|---|
| name | string | 关键词 |
| country | string | 市场代码 |
| monthlySearchVolumeExact | integer | 月精确匹配搜索量 |
| monthlySearchVolumeBroad | integer | 月广泛匹配搜索量 |
| monthlyTrend | float | 月环比趋势(%) |
| quarterlyTrend | float | 季度趋势(%) |
| dominantCategory | string | 主要类目 |
| relevancyScore | integer | 关键词与 ASIN 的相关度(0-100) |
| easeOfRankingScore | integer | 排名容易程度(0-100,越高越容易排名) |
| organicRank | integer | ASIN 的自然搜索排名 |
| sponsoredRank | integer | ASIN 的广告排名 |
| overallRank | integer | 综合排名位置 |
| organicProductCount | integer | 自然搜索结果中的商品总数 |
| sponsoredProductCount | integer | 广告位商品总数 |
| ppcBidExact | float | 精确匹配 PPC 建议出价(USD) |
| ppcBidBroad | float | 广泛匹配 PPC 建议出价(USD) |
| spBrandAdBid | float | SP 品牌广告建议出价(USD) |
| recommendedPromotions | integer | 推荐促销量 |
| primaryAsin | string | 该关键词下排名最高的 ASIN |
| relativeOrganicPosition | float | 查询 ASIN 的自然排名相对位置 |
| relativeSponsoredPosition | float | 查询 ASIN 的广告排名相对位置 |
| organicRankingAsinsCount | integer | 有自然排名的查询 ASIN 数量 |
| sponsoredRankingAsinsCount | integer | 有广告排名的查询 ASIN 数量 |
| avgCompetitorOrganicRank | float | 查询 ASIN 的平均自然排名 |
| avgCompetitorSponsoredRank | float | 查询 ASIN 的平均广告排名 |
| variationLowestOrganicRank | integer | 变体中最佳自然排名 |
| variationLowestSponsoredRank | integer | 变体中最佳广告排名 |
| competitorOrganicRank | array | 各 ASIN 的自然排名,元素为 {asin, organicRank} |
| competitorSponsoredRank | array | 各 ASIN 的广告排名,元素为 {asin, sponsoredRank} |
| updatedAt | string | 数据最后更新时间 |
错误码
正常情况下,接口的 HTTP 状态码均为 200,业务的成功与否通过响应体中的 errorCode 字段区分(errorCode = 200 表示成功,其他值表示业务错误)。当遇到未授权等情况时,HTTP 状态码为 401,且对应的 errorCode 也是 401。
| errcode | 含义 | 处理建议 |
|---|---|---|
| 200 | 成功 | 正常解析 keywordInfoList |
| 401 | 认证失败 | 检查请求头 Authorization 是否正确携带 API Key |
| 其他非200值 | 业务异常 | 参考 errmsg 字段获取具体错误原因 |
错误响应示例:
{
"errcode": 401,
"errmsg": "authorized error"
}curl 示例
curl -X POST https://tool-gateway.linkfox.com/tool-jungle-scout/keywords/by-asin \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"marketplace": "us", "asins": ["B0DXXXXXXX"], "needCount": 50, "sort": "-monthly_search_volume_exact"}'响应示例
{
"costToken": 10,
"keywordInfoList": [
{
"name": "yoga mat",
"country": "us",
"monthlySearchVolumeExact": 85420,
"monthlySearchVolumeBroad": 125000,
"monthlyTrend": 12.5,
"quarterlyTrend": 8.3,
"dominantCategory": "Sports & Outdoors",
"relevancyScore": 95,
"easeOfRankingScore": 42,
"organicRank": 5,
"sponsoredRank": 3,
"overallRank": 4,
"organicProductCount": 2000,
"sponsoredProductCount": 48,
"ppcBidExact": 1.25,
"ppcBidBroad": 0.95,
"spBrandAdBid": 2.10,
"recommendedPromotions": 5,
"primaryAsin": "B0DXXXXXXX",
"relativeOrganicPosition": 0.12,
"relativeSponsoredPosition": 0.08,
"organicRankingAsinsCount": 1,
"sponsoredRankingAsinsCount": 1,
"avgCompetitorOrganicRank": 5.0,
"avgCompetitorSponsoredRank": 3.0,
"variationLowestOrganicRank": 3,
"variationLowestSponsoredRank": 2,
"competitorOrganicRank": [{"asin": "B0DXXXXXXX", "organicRank": 5}],
"competitorSponsoredRank": [{"asin": "B0DXXXXXXX", "sponsoredRank": 3}],
"updatedAt": "2026-04-10"
}
]
}---
Feedback API
This endpoint is separate from the tool API above. Do not mix the two base URLs.
- POST
https://skill-api.linkfox.com/api/v1/public/feedback - Content-Type:
application/json
{
"skillName": "linkfox-junglescout-keyword-by-asin",
"sentiment": "POSITIVE",
"category": "OTHER",
"content": "Results were accurate, user was satisfied."
}Field rules:
skillName: Use this skill'snamefrom the YAML frontmattersentiment: Choose ONE —POSITIVE(praise),NEUTRAL(suggestion without emotion),NEGATIVE(complaint or error)category: Choose ONE —BUG(malfunction or wrong data),COMPLAINT(user dissatisfaction),SUGGESTION(improvement idea),OTHERcontent: Include what the user said or intended, what actually happened, and why it is a problem or praise
#!/usr/bin/env python3
"""
Jungle Scout — 根据 ASIN 反查关键词 - LinkFox Skill
Calls the tool-jungle-scout/keywords/by-asin API endpoint
Usage:
python junglescout_keyword_by_asin.py '{"marketplace": "us", "asins": ["B0DXXXXXXX"], "needCount": 50}'
"""
import json
import os
import sys
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
API_URL = "https://tool-gateway.linkfox.com/tool-jungle-scout/keywords/by-asin"
REQUIRED_PARAMS = ["marketplace", "asins"]
VALID_MARKETPLACES = {"us", "uk", "de", "in", "ca", "fr", "it", "es", "mx", "jp"}
MAX_ASINS = 10
def get_api_key():
"""Retrieve the API key from environment, with a friendly prompt if missing."""
key = os.environ.get("LINKFOXAGENT_API_KEY")
if not key:
print(
"API Key not configured. Please complete authorization first:\n"
"1. Visit https://skill.linkfox.com/linkfoxskills/guide.htm to obtain your Key\n"
"2. Set the environment variable: export LINKFOXAGENT_API_KEY=your-key-here",
file=sys.stderr,
)
sys.exit(1)
return key
def validate_params(params: dict):
"""Validate required parameters and constraints."""
missing = [p for p in REQUIRED_PARAMS if p not in params]
if missing:
print(f"Error: missing required parameters: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
marketplace = params.get("marketplace", "")
if marketplace not in VALID_MARKETPLACES:
print(
f"Error: invalid marketplace '{marketplace}'. "
f"Valid values: {', '.join(sorted(VALID_MARKETPLACES))}",
file=sys.stderr,
)
sys.exit(1)
asins = params.get("asins", [])
if not isinstance(asins, list) or len(asins) == 0:
print("Error: 'asins' must be a non-empty array of ASIN strings", file=sys.stderr)
sys.exit(1)
if len(asins) > MAX_ASINS:
print(f"Error: maximum {MAX_ASINS} ASINs per request, got {len(asins)}", file=sys.stderr)
sys.exit(1)
def call_api(params: dict) -> dict:
"""Call the tool gateway API."""
api_key = get_api_key()
data = json.dumps(params).encode("utf-8")
req = Request(
API_URL,
data=data,
headers={
"Authorization": api_key,
"Content-Type": "application/json",
"User-Agent": "LinkFox-Skill/1.0",
},
method="POST",
)
try:
with urlopen(req, timeout=120) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as e:
body = e.read().decode("utf-8") if e.fp else ""
return {"error": f"HTTP {e.code}: {e.reason}", "details": body}
except URLError as e:
return {"error": f"Connection failed: {e.reason}"}
def main():
if len(sys.argv) < 2:
print("Usage: junglescout_keyword_by_asin.py '<JSON parameters>'", file=sys.stderr)
print(
'Example: junglescout_keyword_by_asin.py \'{"marketplace": "us", '
'"asins": ["B0DXXXXXXX"], "needCount": 50}\'',
file=sys.stderr,
)
sys.exit(1)
try:
params = json.loads(sys.argv[1])
except json.JSONDecodeError as e:
print(f"Invalid parameter format: {e}", file=sys.stderr)
sys.exit(1)
validate_params(params)
result = call_api(params)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Skill response I/O helper — wraps any main script to persist large API
responses to disk, then offers a `read` subcommand to extract specific fields
from those persisted files. Generic, business-agnostic.
This script is bundled into each skill's scripts/ directory by tools/response_io/sync.py.
The agent must pass --script <path> to identify which main script to execute.
Usage:
python scripts/response_io.py run --script <PATH> --out-dir <DIR> '<json_params>' [--label NAME] [--timeout SEC]
python scripts/response_io.py read <file> (--path "<JMESPath>" | --fields "f1,f2,...") [--limit N] [--offset M] [--format json|jsonl|csv|table]
"""
from __future__ import annotations
import sys
if sys.version_info < (3, 10):
sys.exit(
"Error: Python 3.10+ required (current: "
f"{sys.version_info.major}.{sys.version_info.minor}). "
"Please upgrade Python."
)
import argparse
import csv
import io
import json
import os
import re
import secrets
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any
# Force UTF-8 stdout/stderr so non-ASCII chars in previews and API responses
# print correctly on Windows (default cp936 / gbk).
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except (AttributeError, OSError):
pass
try:
import jmespath # type: ignore
HAS_JMESPATH = True
except ImportError:
HAS_JMESPATH = False
MAX_STRING_LEN = 120
MAX_DEPTH = 3
SAMPLE_KEY_CAP = 15
RAW_TEXT_PEEK = 500
DEFAULT_TIMEOUT_SEC = 300
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _err(msg: str, code: int = 1) -> None:
print(msg, file=sys.stderr)
sys.exit(code)
def _resolve_script(script_arg: str) -> Path:
p = Path(script_arg).expanduser()
if not p.is_absolute():
# Resolve relative to the current working directory the agent invoked from.
p = (Path.cwd() / p).resolve()
else:
p = p.resolve()
if not p.is_file():
_err(f"--script path not found: {p}")
return p
def _resolve_skill_name(main_script: Path) -> str:
"""Best-effort skill name extraction for filename prefixing.
main_script lives at <skill_dir>/scripts/<name>.py — return <skill_dir>'s
folder name. Fall back to the script's stem if structure differs.
"""
try:
if main_script.parent.name == "scripts":
return main_script.parents[1].name
except IndexError:
pass
return main_script.stem
def _sanitize_label(label: str) -> str:
"""Allow only safe filename chars in --label to prevent path traversal."""
cleaned = re.sub(r"[^\w\-]", "_", label)
return cleaned[:64] # cap length
def _truncate_string(s: str) -> str:
if len(s) <= MAX_STRING_LEN:
return s
return s[:MAX_STRING_LEN] + f"...(truncated, total {len(s)} chars)"
def _truncate_value(value: Any, depth: int = 0) -> Any:
"""Recursively truncate strings, deep nesting, and large arrays for preview."""
if depth >= MAX_DEPTH:
if isinstance(value, dict):
return f"<truncated nested object, keys: {list(value.keys())[:10]}>"
if isinstance(value, list):
return f"<truncated nested array, length: {len(value)}>"
if isinstance(value, str):
return _truncate_string(value)
return value
if isinstance(value, str):
return _truncate_string(value)
if isinstance(value, dict):
out = {k: _truncate_value(v, depth + 1) for k, v in value.items()}
return out
if isinstance(value, list):
if not value:
return []
truncated = [_truncate_value(value[0], depth + 1)]
if len(value) > 1:
# Note total length on the parent — keep the array type-homogeneous
# so downstream consumers can iterate without special-casing strings.
truncated.append({"_omitted_items": len(value) - 1})
return truncated
return value
def _shape_of(value: Any, top: bool = False) -> Any:
"""Lightweight schema description for the preview block."""
if isinstance(value, dict):
keys = list(value.keys())
out: dict[str, Any] = {"type": "object", "top_keys" if top else "keys": keys}
if top:
for k in keys[:8]:
out[k] = _shape_of(value[k])
return out
if isinstance(value, list):
out = {"type": "array", "length": len(value)}
if value and isinstance(value[0], dict):
out["item_keys"] = list(value[0].keys())
elif value:
out["item_type"] = type(value[0]).__name__
return out
return {"type": type(value).__name__}
def _build_sample(value: Any) -> Any:
"""First-record sample with explicit truncation marker."""
if isinstance(value, list):
if not value:
return {"_truncated_record": True, "_note": "array is empty"}
first = value[0]
if isinstance(first, dict):
sample = {"_truncated_record": True, "_note": f"first of {len(value)} items"}
sample.update(_truncate_value(first, depth=1))
return sample
return {"_truncated_record": True, "_note": f"first of {len(value)} items", "value": _truncate_value(first, depth=1)}
if isinstance(value, dict):
sample = {"_truncated_record": True, "_note": "top-level object (truncated)"}
sample.update(_truncate_value(value, depth=1))
return sample
return {"_truncated_record": True, "value": _truncate_value(value, depth=1)}
def _shrink_preview(preview: dict) -> dict:
"""Cap the sample's value fields when it has many keys.
`shape.*.item_keys` is the single source of truth for the full key list
(always complete, no truncation). The sample only ever shows up to
SAMPLE_KEY_CAP fields with their concrete values, since the agent only
needs a feel for value shapes — for the full menu of available fields,
they read `shape`.
"""
sample = preview.get("sample")
if isinstance(sample, dict):
meta_keys = {"_truncated_record", "_note"}
data_keys = [k for k in sample.keys() if k not in meta_keys]
if len(data_keys) > SAMPLE_KEY_CAP:
kept = data_keys[:SAMPLE_KEY_CAP]
new_sample = {k: v for k, v in sample.items() if k in meta_keys or k in kept}
base_note = sample.get("_note", "")
extra = (
f"showing first {SAMPLE_KEY_CAP} of {len(data_keys)} fields "
f"(see `shape` for the complete key list)"
)
new_sample["_note"] = f"{base_note}; {extra}" if base_note else extra
preview["sample"] = new_sample
return preview
# ---------------------------------------------------------------------------
# `run` subcommand
# ---------------------------------------------------------------------------
def cmd_run(args: argparse.Namespace) -> int:
main_script = _resolve_script(args.script)
skill_name = _resolve_skill_name(main_script)
out_dir = Path(args.out_dir).expanduser().resolve()
try:
out_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
_err(f"Failed to create --out-dir {out_dir}: {e}")
if not os.access(out_dir, os.W_OK):
_err(f"--out-dir is not writable: {out_dir}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
rand = secrets.token_hex(3)
safe_label = _sanitize_label(args.label) if args.label else ""
label_part = f"__{safe_label}" if safe_label else ""
out_file = out_dir / f"{skill_name}__{timestamp}_{rand}{label_part}.json"
# Force the child process to emit UTF-8 regardless of the host console
# encoding (Windows defaults to cp936 / gbk and would otherwise corrupt
# non-ASCII bytes when we read them back).
child_env = os.environ.copy()
child_env["PYTHONIOENCODING"] = "utf-8"
timed_out = False
try:
proc = subprocess.run(
[sys.executable, str(main_script), args.params],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=child_env,
timeout=args.timeout,
)
stdout_text = proc.stdout or ""
stderr_text = proc.stderr or ""
returncode = proc.returncode
except subprocess.TimeoutExpired as e:
timed_out = True
stdout_text = (e.stdout.decode("utf-8", errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "")) or ""
stderr_text = (e.stderr.decode("utf-8", errors="replace") if isinstance(e.stderr, bytes) else (e.stderr or "")) or ""
returncode = 124 # convention for timeout
# Always write the captured stdout to disk, even if not JSON.
try:
out_file.write_text(stdout_text, encoding="utf-8")
except OSError as e:
_err(f"Failed to write output file {out_file}: {e}")
if stderr_text:
sys.stderr.write(stderr_text)
# Try to parse the captured stdout as JSON for the preview.
try:
parsed = json.loads(stdout_text) if stdout_text.strip() else None
format_kind = "json"
except json.JSONDecodeError:
parsed = None
format_kind = "raw_text"
preview: dict[str, Any] = {
"_preview": {
"is_preview": True,
"warning": (
"PREVIEW ONLY — NOT FULL DATA. The full response is saved to `file`. "
"Use `python scripts/response_io.py read <file> --fields '...'` to extract "
"specific fields, or `--path '<JMESPath>'` for complex projections."
),
},
}
# Surface failures prominently so agents don't mistake a stub preview for success.
if returncode != 0 or timed_out:
stderr_snippet = stderr_text[-500:] if stderr_text else ""
preview["_error"] = {
"exit_code": returncode,
"timed_out": timed_out,
"stderr_snippet": stderr_snippet,
"hint": "The wrapped script failed or timed out. The output file may be empty or partial.",
}
preview.update({
"file": str(out_file),
"size_bytes": out_file.stat().st_size,
"skill": skill_name,
"exit_code": returncode,
"format": format_kind,
"label": safe_label or None,
"next_steps_hint": (
"use: python scripts/response_io.py read <file> --fields '...' | --path '...'"
),
})
if format_kind == "json":
preview["shape"] = _shape_of(parsed, top=True)
preview["sample"] = _build_sample(parsed)
else:
peek = stdout_text[:RAW_TEXT_PEEK]
preview["raw_text_peek"] = peek
preview["raw_text_total_chars"] = len(stdout_text)
preview["sample"] = {
"_truncated_record": True,
"_note": f"stdout was not valid JSON; first {RAW_TEXT_PEEK} chars shown above in raw_text_peek",
}
preview = _shrink_preview(preview)
print(json.dumps(preview, ensure_ascii=False, indent=2))
return returncode
# ---------------------------------------------------------------------------
# `read` subcommand
# ---------------------------------------------------------------------------
def _load_json(path: Path) -> Any:
try:
text = path.read_text(encoding="utf-8")
except OSError as e:
_err(f"Failed to read file {path}: {e}")
try:
return json.loads(text)
except json.JSONDecodeError as e:
_err(f"File is not valid JSON: {path}\n{e}")
def _basic_dot_path(data: Any, path: str) -> Any:
"""Pure-stdlib dot-path resolver. No [*] support — callers fall back here only when jmespath is unavailable AND the path has no [*]."""
cur = data
for part in path.split("."):
if isinstance(cur, dict):
cur = cur.get(part)
else:
return None
return cur
def _resolve_field(data: Any, expr: str) -> Any:
if HAS_JMESPATH:
return jmespath.search(expr, data)
if "[" in expr or "*" in expr:
_err(
f"jmespath is required for expression '{expr}'. "
f"Install with: pip install jmespath"
)
return _basic_dot_path(data, expr)
def _project_fields(data: Any, fields: list[str]) -> Any:
"""Run each field expr; if any returns a list, zip them into list-of-dicts."""
resolved: dict[str, Any] = {f: _resolve_field(data, f) for f in fields}
list_lengths = [len(v) for v in resolved.values() if isinstance(v, list)]
if not list_lengths:
return resolved
# All list values must be same length to zip cleanly.
if len(set(list_lengths)) > 1:
# Fallback: return the dict as-is so caller can inspect mismatches.
return resolved
n = list_lengths[0]
rows = []
for i in range(n):
row = {}
for f, v in resolved.items():
row[f] = v[i] if isinstance(v, list) else v
rows.append(row)
return rows
def _apply_slice(value: Any, limit: int | None, offset: int | None) -> Any:
if not isinstance(value, list):
return value
start = offset or 0
end = (start + limit) if limit is not None else None
return value[start:end]
def _format_output(value: Any, fmt: str) -> str:
if fmt == "json":
return json.dumps(value, ensure_ascii=False, indent=2)
if fmt == "jsonl":
if isinstance(value, list):
return "\n".join(json.dumps(item, ensure_ascii=False) for item in value)
return json.dumps(value, ensure_ascii=False)
if fmt in ("csv", "table"):
if not isinstance(value, list) or not value:
_err(f"--format {fmt} requires a non-empty list result")
if not all(isinstance(item, dict) for item in value):
_err(f"--format {fmt} requires list-of-objects, got list of {type(value[0]).__name__}")
keys: list[str] = []
for item in value:
for k in item.keys():
if k not in keys:
keys.append(k)
if fmt == "csv":
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=keys, extrasaction="ignore")
writer.writeheader()
for item in value:
writer.writerow({k: _stringify(item.get(k)) for k in keys})
return buf.getvalue().rstrip("\n")
# table: simple aligned columns
rows = [[_stringify(item.get(k)) for k in keys] for item in value]
widths = [len(k) for k in keys]
for row in rows:
for i, cell in enumerate(row):
widths[i] = max(widths[i], len(cell))
lines = [
" ".join(k.ljust(widths[i]) for i, k in enumerate(keys)),
" ".join("-" * widths[i] for i in range(len(keys))),
]
for row in rows:
lines.append(" ".join(row[i].ljust(widths[i]) for i in range(len(keys))))
return "\n".join(lines)
_err(f"Unknown --format: {fmt}")
return "" # unreachable
def _stringify(v: Any) -> str:
if v is None:
return ""
if isinstance(v, (dict, list)):
return json.dumps(v, ensure_ascii=False)
return str(v)
def cmd_read(args: argparse.Namespace) -> int:
if not args.path and not args.fields:
_err("read: either --path or --fields is required")
if args.path and args.fields:
_err("read: --path and --fields are mutually exclusive")
file_path = Path(args.file).expanduser().resolve()
data = _load_json(file_path)
if args.path:
result = _resolve_field(data, args.path)
else:
fields = [f.strip() for f in args.fields.split(",") if f.strip()]
if not fields:
_err("--fields parsed to empty list")
result = _project_fields(data, fields)
result = _apply_slice(result, args.limit, args.offset)
print(_format_output(result, args.format))
return 0
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
prog="response_io.py",
description="Persist large skill API responses to disk and read fields on demand.",
)
sub = parser.add_subparsers(dest="cmd", required=True)
p_run = sub.add_parser(
"run",
help="Execute a main script and persist its stdout to a file; "
"print only a lightweight preview to stdout.",
)
p_run.add_argument("params", help="JSON params string passed verbatim to the main script (argv[1]).")
p_run.add_argument("--script", required=True, help="Path to the main script to execute, e.g. scripts/my_api.py")
p_run.add_argument("--out-dir", required=True, help="Directory to write the response file into (created if missing).")
p_run.add_argument("--label", default=None, help="Optional filename suffix; sanitized to safe filename characters.")
p_run.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SEC, help=f"Subprocess timeout in seconds (default: {DEFAULT_TIMEOUT_SEC}).")
p_run.set_defaults(func=cmd_run)
p_read = sub.add_parser(
"read",
help="Extract specific fields from a previously persisted response file.",
)
p_read.add_argument("file", help="Path to the persisted JSON response file.")
g = p_read.add_mutually_exclusive_group()
g.add_argument("--path", default=None, help="JMESPath expression, e.g. 'data[*].{asin: asin, title: title}'.")
g.add_argument("--fields", default=None, help="Comma-separated field paths, e.g. 'data[*].asin,data[*].title'.")
p_read.add_argument("--limit", type=int, default=None, help="Take at most N items (when result is a list).")
p_read.add_argument("--offset", type=int, default=None, help="Skip the first M items (when result is a list).")
p_read.add_argument("--format", choices=["json", "jsonl", "csv", "table"], default="json", help="Output format (default: json).")
p_read.set_defaults(func=cmd_read)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Use this skill for ASIN-driven keyword discovery with rank and bid metrics—not for text-based keyword search or historical ABA trend analysis.
FAQ
How many ASINs can linkfox-junglescout-keyword-by-asin query at once?
linkfox-junglescout-keyword-by-asin accepts up to 10 ASINs per call as a standard 10-character Amazon ASIN array. Both marketplace and asins are required; default marketplace is us when unspecified.
Which Amazon marketplaces does the skill support?
linkfox-junglescout-keyword-by-asin supports 10 marketplaces: us, uk, de, in, ca, fr, it, es, mx, and jp. Map user phrases like "日本站" to jp and "德国站" to de before calling the API.