
Linkfox Sellersprite Competitor
- 170 installs
- 64 repo stars
- Updated August 3, 2026
- linkfox-ai/linkfox-skills
Research Amazon marketplace competitors via SellerSprite-style signals—ASIN overlap, keyword gaps, pricing bands, and listing patterns—before launching or repositioning a product.
About
Guides Claude through SellerSprite-style Amazon seller competitor research: identifying rival ASINs, comparing keywords, prices, reviews, and listing structure so teams can validate niches and craft differentiation before sourcing or launching SKUs.
- SellerSprite-oriented competitor workflows
- Amazon ASIN and keyword gap analysis
- Pricing and listing pattern comparison
- Niche validation before inventory commits
- Positioning inputs for go-to-market
Linkfox Sellersprite Competitor by the numbers
- 170 all-time installs (skills.sh)
- Ranked #357 of 853 Sales & Marketing 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-sellersprite-competitorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 170 |
|---|---|
| repo stars | ★ 64 |
| Last updated | August 3, 2026 |
| Repository | linkfox-ai/linkfox-skills ↗ |
What it does
Research Amazon marketplace competitors via SellerSprite-style signals—ASIN overlap, keyword gaps, pricing bands, and listing patterns—before launching or repositioning a product.
Files
SellerSprite Competitor Lookup
This skill guides you on how to query and analyze Amazon competitor product data, helping Amazon sellers discover competing products, benchmark performance, and extract actionable competitive intelligence.
Core Concepts
The SellerSprite Competitor Lookup tool provides comprehensive Amazon product data across 12 marketplaces. It allows querying products by ASIN, keyword, seller name, brand, or category, and returns detailed metrics including monthly sales volume, revenue, BSR ranking, pricing, ratings, and growth trends.
Data snapshots: The tool supports both real-time data (last 30 days) and historical monthly snapshots. Use nearly (default) for current data or a yyyyMM format (e.g., 202501) for historical snapshots. Historical snapshots capture all active listings for that month, enabling year-over-year and seasonal comparisons.
Category hierarchy: Amazon category names support multi-level paths separated by colons (:). For example, Electronics:Computers & Accessories:Monitors. Convert user-provided category descriptions into the proper colon-separated format.
Supported Marketplaces
US (United States), UK (United Kingdom), DE (Germany), FR (France), JP (Japan), CA (Canada), IT (Italy), ES (Spain), MX (Mexico), AU (Australia), TR (Turkey), IN (India)
Default marketplace is US. Use US when the user does not specify a marketplace.
Parameter Guide
Search Filters
| Parameter | Description | Example |
|---|---|---|
| marketplace | Amazon marketplace code | US, UK, DE, JP |
| keyword | Search keyword (translate to the marketplace language) | wireless earbuds |
| asinList | One or more ASINs, comma-separated (max 40) | B072MQ5BRX,B08N5WRWNW |
| sellerName | Seller name to filter by | Anker Direct |
| brand | Brand name to filter by | Anker |
| nodeLabel | Amazon category name (colon-separated levels) | Electronics:Headphones |
| nodeIdPath | Amazon category ID path | 172282 |
| matchType | Keyword match mode: 1 = phrase, 2 = fuzzy, 3 = exact (default 1) | 1 |
| showVariation | Show product variations: Y or N (default N) | N |
| dataSnapshotMonth | Data snapshot month (nearly for real-time, or yyyyMM) | nearly |
Pagination & Sorting
| Parameter | Description | Example |
|---|---|---|
| page | Page number, starting from 1 | 1 |
| size | Results per page, 10-100 (default 50) | 50 |
| order.field | Sort field (see sort options below) | total_units |
| order.desc | Sort direction: true = descending, false = ascending | true |
Sort Field Options
| Field | Description |
|---|---|
| total_units | Monthly sales units |
| total_amount | Monthly sales revenue |
| bsr_rank | BSR ranking |
| price | Price |
| rating | Rating score |
| reviews | Number of reviews |
| profit | Gross margin |
| reviews_rate | Review rate |
| available_date | Listing date |
| questions | Q&A count |
| total_units_growth | Monthly sales unit growth rate |
| total_amount_growth | Monthly revenue growth rate |
| reviews_increasement | Monthly new reviews |
| bsr_rank_cv | 7-day BSR growth count |
| bsr_rank_cr | 7-day BSR growth rate |
| amz_unit | Variant sales units |
Key Response Fields
| Field | Description |
|---|---|
| asin | Product ASIN |
| title | Product title |
| price | Current price |
| monthlySalesUnits | Monthly sales volume |
| monthlySalesRevenue | Monthly sales revenue |
| bsr | BSR ranking |
| bsrGrowthRate | BSR growth rate |
| bsrGrowthCount | BSR growth count |
| rating | Rating score |
| ratings | Number of ratings |
| ratingsGrowth | Monthly new ratings |
| ratingsRate | Review rate |
| brand | Brand name |
| sellerName | BuyBox seller |
| sellerNation | BuyBox seller nationality |
| fulfillment | Fulfillment type (AMZ/FBA/FBM) |
| availableDateString | Listing date |
| profit | Gross margin |
| nodeLabelPath | Category path |
| imageUrl | Product image URL |
| monthlySalesUnitsGrowthRate | Monthly sales growth rate |
| listingQualityScore | Listing quality score |
| variationNum | Number of variations |
| parent | Parent ASIN |
| badgeBestSeller | Best Seller badge (Y/N) |
| badgeAmazonChoice | Amazon's Choice badge (Y/N) |
| badgeEbc | A+ Content (Y/N) |
| badgeVideo | Video present (Y/N) |
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/sellersprite_competitor_lookup.py directly to run queries.
Usage Examples
1. Look up competitors by ASIN
{
"marketplace": "US",
"asinList": "B072MQ5BRX,B08N5WRWNW"
}Use case: Analyze specific competing products by their ASINs.
2. Search competitors by keyword
{
"marketplace": "US",
"keyword": "wireless earbuds",
"matchType": 1,
"order": {"field": "total_units", "desc": "true"},
"size": 20
}Use case: Discover top-selling products for a keyword, sorted by monthly sales.
3. Filter by brand and category
{
"marketplace": "US",
"brand": "Anker",
"nodeLabel": "Electronics:Headphones",
"order": {"field": "total_amount", "desc": "true"}
}Use case: Analyze a specific brand's product lineup within a category.
4. Find products by seller name
{
"marketplace": "DE",
"sellerName": "Anker Direct",
"order": {"field": "bsr_rank", "desc": "false"}
}Use case: View all products from a particular seller sorted by BSR.
5. Historical snapshot comparison
{
"marketplace": "US",
"keyword": "space heater",
"dataSnapshotMonth": "202412",
"order": {"field": "total_units", "desc": "true"},
"size": 20
}Use case: Analyze seasonal product performance using historical data snapshots.
6. Show product variations
{
"marketplace": "JP",
"asinList": "B0XXXXXXXXX",
"showVariation": "Y"
}Use case: Examine all variation-level data for a product family.
Display Rules
1. Present data clearly: Show query results in well-formatted tables. Include key metrics such as ASIN, title, price, monthly sales, BSR, rating, and brand. Do not provide subjective business advice unless the user asks for it. 2. Keyword language: When searching by keyword, always translate the keyword to the target marketplace language (e.g., English for US/UK, German for DE, Japanese for JP). Remind the user of this if they provide keywords in the wrong language. 3. BSR clarification: When displaying BSR data, remind users that a lower BSR value indicates stronger sales performance. 4. Growth metrics: When showing growth rates, clarify whether positive values mean improvement or decline (positive BSR growth count means BSR increased, which means worsened ranking). 5. Pagination notice: When the total result count exceeds the returned page size, inform the user of the total count and offer to fetch additional pages. 6. Badge highlights: When products carry badges (Best Seller, Amazon's Choice, A+ Content, Video), highlight these in the results as they are important competitive signals. 7. Error handling: When a query fails, explain the reason based on the message field and suggest adjusting query parameters. 8. Snapshot guidance: When users want to do seasonal or trend analysis, proactively suggest using historical snapshots (e.g., last year's same month) for comparison.
Important Limitations
- Result cap: Each page returns 10-100 records (controlled by
size). Use pagination for larger result sets. - ASIN limit: A maximum of 40 ASINs can be queried at once via
asinList. - Historical snapshots: Only existing monthly snapshots can be queried; future dates are not supported.
- Keyword language: Keywords should match the marketplace language for best results.
User Expression & Scenario Quick Reference
Applicable -- Amazon competitor product data queries:
| User Says | Scenario |
|---|---|
| "Find competitors for this ASIN" | ASIN-based competitor lookup |
| "Top sellers for wireless earbuds" | Keyword-based product discovery |
| "What is this seller selling" | Seller product portfolio analysis |
| "Show me products in Electronics category" | Category-based browsing |
| "Monthly sales for these ASINs" | Sales estimation for specific products |
| "New products gaining traction" | Growth trend detection |
| "Compare products across brands" | Brand benchmarking |
| "How was this niche last December" | Historical snapshot analysis |
| "Best sellers with high ratings" | Multi-metric filtering |
| "FBA vs FBM in this category" | Fulfillment type analysis |
Not applicable -- Needs beyond competitor product data:
- ABA search term data or keyword ranking (use ABA Data Explorer instead)
- Advertising / PPC campaign management
- Product reviews content or sentiment analysis
- Listing copywriting or optimization suggestions
- Supplier sourcing or manufacturing costs
- Account health or policy compliance
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/sellersprite_competitor_lookup.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, set [LinkFox Skills](https://skill.linkfox.com/).
卖家精灵-查竞品 API 参考
调用规范
- 请求地址:
https://tool-gateway.linkfox.com/sellersprite/competitor-lookup - 请求方式: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。可选值:US、UK、DE、FR、JP、CA、IT、ES、MX、AU、TR、IN |
| keyword | string | 否 | 搜索关键词。请尽量翻译为对应国家的语言,比如美国用英语关键词,德国用德语关键词等 |
| asinList | string | 否 | ASIN,多个ASIN使用英文逗号分隔,最多40个。格式:^[A-Z0-9]+(,[A-Z0-9]+){0,39}$ |
| sellerName | string | 否 | 卖家名称筛选 |
| brand | string | 否 | 品牌名称筛选 |
| nodeLabel | string | 否 | 亚马逊类目名称,支持多层级类目名称,层级之间用英文冒号 : 分割,例如 Electronics:Headphones |
| nodeIdPath | string | 否 | 亚马逊类目ID路径 |
| matchType | integer | 否 | 匹配方式。1 = 词组匹配(默认),2 = 模糊匹配,3 = 精准匹配 |
| showVariation | string | 否 | 是否查询变体。Y = 是,N = 否(默认) |
| dataSnapshotMonth | string | 否 | 亚马逊商品数据快照年月。默认 nearly(查询最近30天实时数据)。使用 yyyyMM 格式查询历史快照(如 202412 表示2024年12月)。仅支持已存在的历史快照,不支持未来日期。建议季节性分析时查询去年同期快照进行对比 |
| page | integer | 否 | 页码,从1开始(默认1) |
| size | integer | 否 | 每页条数,返回10-100条数据(默认50) |
| order | object | 否 | 排序配置(见下方说明) |
排序对象(order)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| field | string | 是 | 排序字段。可选值:total_units(月销量)、total_amount(月销售额)、bsr_rank(BSR排名)、price(价格)、rating(评分)、reviews(评分数)、profit(毛利率)、reviews_rate(留评率)、available_date(上架时间)、questions(Q&A数)、total_units_growth(月销量增长率)、total_amount_growth(月销售额增长率)、reviews_increasement(月新增评分数)、bsr_rank_cv(近7天BSR增长数)、bsr_rank_cr(近7天BSR增长率)、amz_unit(子体销量)。默认:total_units |
| desc | string | 是 | 排序方向。true = 降序,false = 升序。默认:true |
响应结构
| 字段 | 类型 | 说明 |
|---|---|---|
| total | integer | 匹配结果总数 |
| sourceType | string | 来源类型(如 amazon) |
| message | string | 执行消息或错误描述 |
| type | string | 渲染样式 |
| nodeLabel | string | 类目名称回显 |
| columns | array | 渲染的列定义 |
| products | array | 竞品列表(见下方说明) |
| costToken | integer | 消耗token |
竞品对象字段(products)
| 字段 | 类型 | 说明 |
|---|---|---|
| asin | string | 商品ASIN |
| title | string | 商品标题 |
| price | number | 当前价格 |
| primePrice | number | Prime价格 |
| averagePrice | number | 平均价格 |
| currency | string | 币种 |
| monthlySalesUnits | integer | 月销量(件数) |
| monthlySalesRevenue | number | 月销售额 |
| monthlySalesUnitsGrowthRate | number | 月销量增长率 |
| bsr | integer | BSR排名 |
| bsrGrowthRate | number | BSR增长率 |
| bsrGrowthCount | integer | BSR增长数 |
| rating | number | 评分 |
| ratings | integer | 评分数 |
| ratingsGrowth | integer | 月新增评分数 |
| ratingsRate | number | 留评率 |
| brand | string | 品牌 |
| brandUrl | string | 品牌URL |
| sellerName | string | BuyBox卖家名称 |
| sellerId | string | BuyBox卖家ID |
| sellerNation | string | BuyBox卖家国籍 |
| sellerNum | integer | 卖家数 |
| fulfillment | string | 配送方式:AMZ、FBA、FBM |
| availableDate | string | 上架时间(日期格式) |
| availableDateString | string | 上架日期(字符串格式) |
| profit | number | 毛利率 |
| fba | number | FBA运费 |
| deliveryPrice | number | 卖家运费 |
| imageUrl | string | 商品图片URL |
| parent | string | 父体ASIN |
| variationNum | integer | 变体数 |
| variant30DayUnits | integer | 子体月销量(件数) |
| variant30DayRevenue | number | 子体月销售额 |
| variant30DayUpdatedAt | string | 子体数据更新时间(时间戳) |
| amzUnitDateString | string | 子体销量更新日期 |
| listingQualityScore | number | Listing质量得分 |
| nodeLabelPath | string | 类目路径 |
| nodeIdPath | string | 节点ID路径 |
| nodeId | integer | 节点ID |
| dimension | string | 商品尺寸 |
| dimensionsType | string | 尺寸类型 |
| weight | string | 商品重量 |
| packageDimensions | string | 包装尺寸 |
| packageDimensionType | string | 包装尺寸类型 |
| packageWeight | string | 包装重量 |
| sku | string | SKU |
| keyword | string | 匹配的关键词(如通过关键词搜索,则显示对应关键词) |
| dataSnapshotMonth | string | 数据查询月份 |
| sourceTool | string | 来源工具 |
| sourceType | string | 来源类型 |
| badgeBestSeller | string | Best Seller标识(Y/N) |
| badgeAmazonChoice | string | Amazon's Choice标识(Y/N) |
| badgeNewRelease | string | New Release标识(Y/N) |
| badgeEbc | string | A+页面(Y/N) |
| badgeVideo | string | 视频介绍(Y/N) |
| badge | object | 标识详情对象,包含:bestSeller、amazonChoice、newRelease、ebc、video(均为 Y/N 字符串) |
| subcategories | array | 子类目排名,每项包含 code(类目code)、rank(排名)、label(名称) |
curl 示例
关键词搜索
curl -X POST https://tool-gateway.linkfox.com/sellersprite/competitor-lookup \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"marketplace": "US", "keyword": "wireless earbuds", "matchType": 1, "size": 20}'ASIN查询
curl -X POST https://tool-gateway.linkfox.com/sellersprite/competitor-lookup \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"marketplace": "US", "asinList": "B072MQ5BRX,B08N5WRWNW"}'按月销售额排序并分页
curl -X POST https://tool-gateway.linkfox.com/sellersprite/competitor-lookup \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"marketplace": "US", "keyword": "phone case", "order": {"field": "total_amount", "desc": "true"}, "page": 1, "size": 50}'历史快照查询
curl -X POST https://tool-gateway.linkfox.com/sellersprite/competitor-lookup \
-H "Authorization: $LINKFOXAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"marketplace": "US", "keyword": "space heater", "dataSnapshotMonth": "202412", "order": {"field": "total_units", "desc": "true"}, "size": 20}'错误码
正常情况下,接口的 HTTP 状态码均为 200,业务的成功与否通过响应体中的 errorCode 字段区分(errorCode = 200 表示成功,其他值表示业务错误)。当遇到未授权等情况时,HTTP 状态码为 401,且对应的 errorCode 也是 401。
| errcode | 含义 | 处理建议 |
|---|---|---|
| 200 | 成功 | 正常解析 products 等业务字段 |
| 401 | 认证失败 | 检查请求头 Authorization 是否正确携带 API Key;API Key 申请方式请参考上述调用规范下的认证方式。 |
| 其他非200值 | 业务异常 | 参考 errmsg 字段获取具体错误原因 |
错误响应示例:
{
"errcode": 401,
"errmsg": "authorized error"
}---
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-xxx-xxx",
"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
"""
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())
#!/usr/bin/env python3
"""
SellerSprite Competitor Lookup - LinkFox Skill
Calls the sellersprite/competitor-lookup API endpoint to query Amazon competitor products.
Usage:
python sellersprite_competitor_lookup.py '{"marketplace": "US", "keyword": "wireless earbuds", "size": 20}'
python sellersprite_competitor_lookup.py '{"marketplace": "US", "asinList": "B072MQ5BRX,B08N5WRWNW"}'
"""
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/sellersprite/competitor-lookup"
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 call_api(params: dict) -> dict:
"""Send a POST request to the competitor lookup API and return the parsed response."""
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=60) 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: sellersprite_competitor_lookup.py '<JSON parameters>'", file=sys.stderr)
print(
"Example: sellersprite_competitor_lookup.py "
"'{\"marketplace\": \"US\", \"keyword\": \"wireless earbuds\", \"size\": 20}'",
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)
result = call_api(params)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()