@nomadamas/k-skill 0.4.2 → 0.4.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomadamas/k-skill",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "k-skill unified CLI: assembles runtime-aware skill instructions and ships bundled helper files",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,219 +1,90 @@
1
1
  # Coupang Product Search
2
2
 
3
- ## What this skill does
3
+ ## Purpose
4
4
 
5
- [retention-corp/coupang_partners](https://github.com/retention-corp/coupang_partners) 저장소의 로컬 Coupang MCP 호환 레이어를 사용해 쿠팡 상품 조회 도구를 실행한다. 기존 유지보수형 HF Space MCP 엔드포인트 대신, 이 저장소의 `bin/coupang_mcp.py`가 제공하는 `local://coupang-mcp` 계약을 호출한다.
5
+ `k-skill-proxy`의 공식 Coupang Partners API route로 쿠팡 상품을 검색한다.
6
+ 사용자에게 쿠팡 파트너스 access/secret key를 요구하지 않는다. 키와 HMAC
7
+ 서명은 proxy 서버에만 존재한다.
6
8
 
7
- - 키워드 상품 검색
8
- - 로켓배송 전용 필터 검색
9
- - 가격대 범위 검색
10
- - 상품 비교표 생성
11
- - 카테고리별 베스트 상품
12
- - 골드박스 당일 특가
13
- - 인기 검색어/계절 상품 추천
9
+ ## Endpoint
14
10
 
15
- ## How it works
16
-
17
- ```
18
- Claude Code / Codex
19
- → coupang-product-search/scripts/coupang_partners_mcp.py
20
- → git clone/update retention-corp/coupang_partners (user cache)
21
- → python3 bin/coupang_mcp.py
22
- → local://coupang-mcp compatible tool layer
23
- ├─ Coupang Partners API client (operator keys present)
24
- └─ hosted fallback → https://a.retn.kr/v1/public/assist (no keys)
25
- ```
26
-
27
- Hard rules:
28
-
29
- - `COUPANG_MCP_ENDPOINT`는 호환성 knob로만 유지한다. 기본값은 `local://coupang-mcp`다.
30
- - 구형 HF Space hosted MCP 엔드포인트를 사용하거나 새로 지어내지 않는다.
31
- - upstream 저장소는 `https://github.com/retention-corp/coupang_partners.git`만 사용한다.
32
- - `tools`와 `init`은 로컬 MCP 계약 확인용으로 먼저 실행한다.
33
-
34
- ## Execution paths
35
-
36
- `retention-corp/coupang_partners`는 하나의 CLI 뒤에서 두 가지 경로를 자동으로 선택한다. 래퍼(`coupang_partners_mcp.py`)는 두 경로 모두를 그대로 통과시킨다.
37
-
38
- 1. **Operator (local HMAC) path** — `COUPANG_ACCESS_KEY`와 `COUPANG_SECRET_KEY`가 둘 다 설정된 경우. upstream이 Coupang Partners API를 HMAC 서명해 직접 호출한다. 키/시크릿은 절대 답변·문서·커밋에 노출하지 않는다.
39
- 2. **Credentialless hosted fallback path** — 위 두 키 중 하나라도 없는 경우(또는 `OPENCLAW_SHOPPING_FORCE_HOSTED=1`). upstream이 자동으로 Retention Corp의 hosted 백엔드(`https://a.retn.kr/v1/public/assist`)로 떨어진다. 이 경로는 `X-OpenClaw-Client-Id` allowlist로 게이트되어 있으며, upstream이 기본으로 실어 보내는 `openclaw-skill` 값이 현재 Retention Corp allowlist에 등록된 값이다. k-skill 래퍼는 `OPENCLAW_SHOPPING_CLIENT_ID`를 별도로 설정하지 않고 이 upstream 기본값을 그대로 사용한다.
40
-
41
- 두 경로 모두 JSON envelope(`ok`/`data.session_id`/`data.tool`/`data.payload`/`data.result`) 모양은 동일하므로, 답변 로직은 경로를 구별할 필요가 없다. short deeplink는 hosted fallback에서는 `https://a.retn.kr/s/...` 형태로, operator path에서는 `https://link.coupang.com/...` 형태로 온다.
42
-
43
- ### 관련 환경변수
44
-
45
- | 환경변수 | 역할 | 기본값 |
46
- |---------|------|--------|
47
- | `COUPANG_ACCESS_KEY`, `COUPANG_SECRET_KEY` | 운영자 Coupang Partners API 크리덴셜. 둘 다 있을 때만 로컬 HMAC 경로가 활성화된다. | 없음 (없으면 hosted fallback) |
48
- | `OPENCLAW_SHOPPING_CLIENT_ID` | hosted fallback이 보낼 `X-OpenClaw-Client-Id`. upstream이 `openclaw-skill`을 기본으로 실어 보내며 이 값이 현재 Retention Corp allowlist에 등록되어 있다. k-skill 래퍼는 이 변수를 오버라이드하지 않는 것을 권장한다. | `openclaw-skill` |
49
- | `OPENCLAW_SHOPPING_FORCE_HOSTED` | `1`이면 키가 있어도 hosted 경로를 강제한다. | 비어있음 |
50
- | `OPENCLAW_SHOPPING_BASE_URL` | hosted 백엔드 base URL 오버라이드. 스테이징/로컬 backend 테스트용. | `https://a.retn.kr` |
51
-
52
- ## MCP endpoint / contract
53
-
54
- ```
55
- local://coupang-mcp
11
+ ```text
12
+ GET ${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}/v1/coupang/products/search
56
13
  ```
57
14
 
58
- 프로토콜 호환 버전: MCP `2025-03-26`. 네트워크로 붙는 Streamable HTTP 서버가 아니라, upstream 저장소의 로컬 MCP 호환 CLI가 같은 도구 이름과 JSON-RPC 모양의 payload를 반환한다.
15
+ 허용 query:
59
16
 
60
- ## When to use
17
+ | field | required | rule |
18
+ | --- | --- | --- |
19
+ | `keyword` 또는 `q` | yes | 2~100자 검색어 |
20
+ | `limit` | no | 1~10, 기본 10 |
21
+ | `subId` | no | 호출 분석용 식별자, 최대 100자 |
61
22
 
62
- - "쿠팡에서 생수 가격 찾아줘"
63
- - "로켓배송 에어팟 찾아줘"
64
- - "20만원 이하 키보드 추천해줘"
65
- - "아이패드 vs 갤럭시탭 비교"
66
- - "오늘 쿠팡 특가 뭐 있어?"
67
- - "전자제품 베스트 보여줘"
23
+ `COUPANG_ACCESS_KEY`와 `COUPANG_SECRET_KEY`를 caller 환경이나 명령 인자로
24
+ 넣지 않는다. 운영자가 proxy의 gpu01 runtime `.env`에만 설정한다.
68
25
 
69
- ## When not to use
70
-
71
- - 돌쇠가 아니며 로그인, 장바구니, 결제 자동화가 필요한 경우
72
- - 돌쇠가 아니며 쿠팡 계정/session 접근이 필요한 경우
73
- - 실시간 재고/품절 여부를 100% 보장해야 하는 경우 (hosted fallback과 Partners API 모두 캐시·지연이 있을 수 있다)
26
+ 상품 링크를 안내할 때는 반드시 "쿠팡 파트너스 활동을 통해 일정액의 수수료를
27
+ 제공받을 수 있습니다."라고 고지한다.
74
28
 
75
29
  ## Workflow
76
30
 
77
- ### 1. Clarify the need
78
-
79
- 검색어가 너무 넓으면 먼저 의도를 좁힌다.
80
-
81
- - 권장 질문: `어떤 용도/예산/브랜드/용량을 우선할까요?`
82
-
83
- ### 2. Bootstrap and check the tool contract
84
-
85
- 래퍼는 기본적으로 `~/.cache/k-skill/coupang_partners`에 upstream 저장소를 clone한다. 이미 clone되어 있으면 그대로 사용하고, 최신화가 필요할 때만 `--update`를 붙인다.
86
-
87
- ```bash
88
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- tools
89
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- init
90
- ```
91
-
92
- 기존 checkout을 명시하거나 CI/검증에서 네트워크 clone을 막으려면:
31
+ 1. 검색어가 넓으면 용도, 예산, 브랜드, 용량을 확인한다.
32
+ 2. 아래처럼 proxy를 호출한다.
93
33
 
94
34
  ```bash
95
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- \
96
- --repo-dir /path/to/coupang_partners \
97
- --no-clone \
98
- tools
99
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- \
100
- --repo-dir /path/to/coupang_partners \
101
- --no-clone \
102
- init
35
+ BASE="${KSKILL_PROXY_BASE_URL:-https://k-skill-proxy.nomadamas.org}"
36
+ curl -fsS --get "${BASE}/v1/coupang/products/search" \
37
+ --data-urlencode 'keyword=무선청소기' \
38
+ --data-urlencode 'limit=10' \
39
+ --data-urlencode 'subId=k-skill'
103
40
  ```
104
41
 
105
- ### 3. Call tools
106
-
107
- 구체적인 사용자 요청에 맞춰 upstream CLI 명령을 호출한다. 결과는 `ok`, `data.tool`, `data.payload`, `data.result`를 포함하는 JSON으로 반환된다.
108
-
109
- ```bash
110
- # 일반 검색 (키 없이도 hosted fallback으로 작동)
111
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- search "32인치 4K 모니터"
112
-
113
- # 로켓배송 필터
114
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- rocket "에어팟"
42
+ 3. 응답의 `items`를 읽고 `is_rocket`에 따라 로켓배송과 일반배송으로 나눈다.
43
+ 4. 사용자의 예산이 있으면 `price`로 필터링하고 상위 3~5개만 비교한다.
44
+ 5. 가격, 품절, 배송 정보는 변할 있음을 명시한다.
45
+ 6. 상품 링크를 제공할 때 아래 affiliate 고지를 반드시 포함한다.
115
46
 
116
- # 가격대 검색
117
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- budget "키보드" --max-price 100000
118
-
119
- # 비교
120
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- compare "아이패드 vs 갤럭시탭"
121
-
122
- # 골드박스 (운영자 키가 필요한 upstream 경로)
123
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- goldbox
47
+ ```text
48
+ 쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 있습니다.
124
49
  ```
125
50
 
126
- ### 4. (optional) hosted fallback 강제
51
+ ## Response fields
127
52
 
128
- 운영자 키가 있는 상태에서도 hosted fallback 경로를 점검하고 싶으면 `OPENCLAW_SHOPPING_FORCE_HOSTED=1`만 추가하면 된다. `OPENCLAW_SHOPPING_CLIENT_ID`는 upstream이 보내는 기본값 `openclaw-skill`이 현재 Retention Corp allowlist에 등록된 값이므로 별도로 설정하지 않는다.
53
+ 상품은 다음 안정 필드를 제공한다.
129
54
 
130
- ```bash
131
- export OPENCLAW_SHOPPING_FORCE_HOSTED=1
132
- npx -y @nomadamas/k-skill@0 exec coupang-product-search scripts/coupang_partners_mcp.py -- search "에어팟"
133
- ```
55
+ - `product_id`
56
+ - `title`
57
+ - `price`, `price_text`
58
+ - `url`, `image_url`
59
+ - `review_count`, `score`
60
+ - `is_rocket`, `is_free_shipping`
134
61
 
135
- ### 5. Continue from discovery to cart and checkout in Dolshoi
136
-
137
- 사용자가 구매나 장바구니 담기를 요청했고 CloakBrowser mode라면 MCP 결과 링크에서 멈추지 않는다.
138
-
139
- 1. 선택한 상품의 공식 쿠팡 URL을 CloakBrowser로 연다.
140
- 2. 현재 상품명, 판매자, 옵션, 수량, 로켓배송 여부, 실시간 가격과 품절 여부를 다시 확인한다.
141
- 3. 로그인이 필요하면 provisioned vault capability를 사용하고, 없으면 `request_vault_credential`로 쿠팡 login을 저장한 뒤 같은 turn에 재개한다.
142
- 4. 옵션과 수량을 선택해 장바구니에 담는다. 장바구니 담기는 가역적이므로 별도 승인 없이 수행하고 실제 담김 상태를 확인한다.
143
- 5. 구매 요청이면 배송지/쿠폰/결제수단 적용 후 최종 주문 직전까지 진행한다.
144
- 6. `clarify`로 상품·옵션·수량·배송지 요약·최종 금액을 승인받고, 승인되면 주문/결제를 실행해 주문번호와 상태를 확인한다.
145
-
146
- CloakBrowser를 사용할 수 없는 generic runtime에서는 기존 검색/링크 제공 범위를 유지한다.
147
-
148
- ## Available tools
149
-
150
- | 도구명 | CLI 명령 | 기능 | 파라미터 예시 |
151
- |--------|----------|------|-------------|
152
- | `search_coupang_products` | `search` | 일반 상품 검색 | `"생수"` |
153
- | `search_coupang_rocket` | `rocket` | 로켓배송만 필터링 | `"에어팟"` |
154
- | `search_coupang_budget` | `budget` | 가격대 범위 검색 | `"키보드" --max-price 100000` |
155
- | `compare_coupang_products` | `compare` | 상품 비교표 생성 | `"아이패드 vs 갤럭시탭"` |
156
- | `get_coupang_recommendations` | `recommendations` | 인기 검색어 제안 | `--category 전자제품` |
157
- | `get_coupang_seasonal` | `seasonal` | 계절/상황별 추천 | `"설날 선물"` |
158
- | `get_coupang_best_products` | `best` | 카테고리별 베스트 | `--category-id 1016` |
159
- | `get_coupang_goldbox` | `goldbox` | 당일 특가 정보 | `--limit 10` |
160
-
161
- 주의: `get_coupang_goldbox`와 `get_coupang_best_products`는 upstream 기준 Coupang Partners API 권한이 필요한 경로이므로, 키가 없는 환경에서는 실패할 수 있다. 이런 경우 에러 메시지를 그대로 전달하고 hosted fallback이 커버하는 `search`/`rocket`/`budget`/`compare` 경로로 우회 제안한다.
162
-
163
- ## Response format
164
-
165
- upstream CLI는 JSON을 출력한다. `data.result` 안의 상품 배열 또는 도구별 객체를 읽고, 답변에서는 로켓배송(rocket)과 일반배송(normal)을 구분한다.
166
-
167
- ```json
168
- {
169
- "ok": true,
170
- "data": {
171
- "session_id": "session-...",
172
- "tool": "search_coupang_products",
173
- "payload": {
174
- "jsonrpc": "2.0",
175
- "result": {
176
- "content": [
177
- {"type": "text", "text": "[...]"}
178
- ]
179
- }
180
- },
181
- "result": []
182
- }
183
- }
184
- ```
62
+ ## Failure modes
185
63
 
186
- 사용자에게 보여줄 때는 다음처럼 짧게 정리한다.
64
+ - `400 bad_request`: 검색어가 없거나 너무 짧음. 입력을 바로잡아 재호출한다.
65
+ - `503 upstream_not_configured`: proxy 운영자가 Coupang key 두 개를 아직 설정하지 않음.
66
+ - `502 upstream_forbidden`: 키가 거절되었거나 Partners API 권한이 없음.
67
+ - `502 upstream_error` / `upstream_unavailable`: 쿠팡 upstream 장애. 실패를 숨기지 말고 나중 재시도를 안내한다.
187
68
 
188
- ```
189
- ## rocket (상위 후보)
190
-
191
- 1) LG전자 4K UHD 모니터
192
- 가격: 397,750원 (참고용)
193
- 보러가기: https://a.retn.kr/s/... # hosted fallback shortlink
194
- 또는: https://link.coupang.com/a/... # operator HMAC 경로 딥링크
69
+ 임의의 Coupang scraping, 구형 HF Space MCP, `a.retn.kr` hosted fallback,
70
+ 사용자 제공 API key로 우회하지 않는다.
195
71
 
196
- ## normal (상위 후보)
197
-
198
- 1) 삼성전자 QHD 오디세이 G5 게이밍 모니터
199
- 가격: 283,000원 (참고용)
200
- 보러가기: https://a.retn.kr/s/...
201
- ```
72
+ ## Continue to cart or purchase
202
73
 
203
- ## Response policy
74
+ 사용자가 장바구니 또는 구매를 요청했고 CloakBrowser mode라면 결과 링크에서
75
+ 멈추지 않는다.
204
76
 
205
- - 후보가 여러 개면 상위 3~5개만 짧게 비교한다.
206
- - 로켓배송/일반배송 구분을 명시한다.
207
- - 가격/품절/배송 정보는 실시간 변동될 있음을 안내한다.
208
- - upstream checkout, 권한, Coupang Partners 환경변수 문제로 실패하면 실패 원인과 재시도/설정 방법을 짧게 안내한다.
209
- - **Affiliate 고지(필수)**: 응답에 포함되는 shortlink(`https://a.retn.kr/s/...`)와 직접 coupang 딥링크(`link.coupang.com/...?lptag=AF...`)는 Retention Corp의 쿠팡 파트너스(affiliate) 채널로 트래킹된다. upstream이 돌려주는 `disclosure` 문자열(`"파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음"`)이 있으면 그대로 노출하고, 없으면 같은 취지의 고지를 답변 말미에 덧붙인다.
77
+ 1. 선택 상품의 공식 쿠팡 URL을 열어 상품명, 판매자, 옵션, 수량, 실시간 가격,
78
+ 로켓배송 여부와 품절 상태를 다시 확인한다.
79
+ 2. 로그인이 필요하면 provisioned vault capability를 사용하고, 없으면
80
+ `request_vault_credential`로 쿠팡 login을 저장한 재개한다.
81
+ 3. 장바구니 담기는 가역적이므로 수행 실제 담김을 확인한다.
82
+ 4. 구매는 배송지, 쿠폰, 결제수단 적용 후 최종 주문 직전에 정확한 대상과 금액을
83
+ `clarify`로 승인받고 실행한다.
210
84
 
211
85
  ## Done when
212
86
 
213
- - `tools`와 `init` 또는 실제 명령으로 retention-corp/coupang_partners 로컬 MCP 계약을 확인했다.
214
- - 검색 결과가 로켓배송/일반배송으로 구분되어 정리되었다.
215
- - 사용자 니즈에 맞는 추천 TOP 3이 제시되었다.
216
- - 가격/배송 정보와 변동 가능성 안내가 포함되었다.
217
- - affiliate 고지(disclosure)가 답변에 포함되었다.
218
- - 돌쇠의 장바구니 요청이면 선택 옵션/수량이 실제 장바구니에 담긴 것을 확인했다.
219
- - 돌쇠의 구매 요청이면 `clarify` 승인 후 주문번호와 결제 상태를 확인했다.
87
+ - proxy 응답을 실제로 받았다.
88
+ - 로켓배송/일반배송과 가격을 구분해 후보를 정리했다.
89
+ - 가격·배송 변동 가능성과 affiliate 고지를 포함했다.
90
+ - 액션 요청이면 해당 표면의 실제 완료 상태를 확인했다.
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "coupang-product-search",
3
- "description": "retention-corp/coupang_partners로컬 Coupang MCP 호환 레이어로 쿠팡 상품 검색, 로켓배송 필터, 가격대 검색, 상품 비교, 베스트 상품, 골드박스 특가를 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
3
+ "description": "k-skill-proxy공식 Coupang Partners API 경로로 쿠팡 상품을 검색하고 로켓배송·가격 후보를 비교한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
4
4
  "profiles": [
5
5
  "vault",
6
6
  "browser",
7
7
  "action:commerce"
8
8
  ],
9
- "frontmatter": "name: coupang-product-search\ndescription: retention-corp/coupang_partners의 로컬 Coupang MCP 호환 레이어로 쿠팡 상품 검색, 로켓배송 필터, 가격대 검색, 상품 비교, 베스트 상품, 골드박스 특가를 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: retail\n locale: ko-KR\n phase: v2"
9
+ "stub_notice": {
10
+ "heading": "Affiliate disclosure (required)",
11
+ "body": "상품 링크를 안내할 때는 반드시 \"쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.\"라고 고지한다."
12
+ },
13
+ "frontmatter": "name: coupang-product-search\ndescription: k-skill-proxy의 공식 Coupang Partners API 경로로 쿠팡 상품을 검색하고 로켓배송·가격 후보를 비교한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: retail\n locale: ko-KR\n phase: v2"
10
14
  }
@@ -26,7 +26,16 @@
26
26
 
27
27
  - 인터넷 연결
28
28
  - `node` 18+
29
- - 이 저장소의 `daiso-product-search` package 또는 동일 로직
29
+ - `daiso-product-search` npm package
30
+
31
+ 설치:
32
+
33
+ ```bash
34
+ npm install daiso-product-search
35
+ ```
36
+
37
+ 이 저장소에서 개발할 때는 루트에서 `npm install` 후 `packages/daiso-product-search`를 쓴다.
38
+ package 설치가 막히면 표면을 직접 호출해 우회하지 말고 설치를 먼저 해결한다. 직접 호출은 실패가 성공처럼 보이는 응답을 준다(Failure modes 참고).
30
39
 
31
40
  ## Required inputs
32
41
 
@@ -161,6 +170,7 @@ console.log(result.pickupStock)
161
170
  - 현재 확인된 공식 표면은 **매장 내 aisle/진열 위치**를 직접 주지 않을 수 있다.
162
171
  - `selStrPkupStck` 403 → `/api/auth/request` 재호출 후 Bearer를 새로 빌드해 재시도한다.
163
172
  - Bearer 재시도 후에도 401/403이면 재고 수량은 `retrievalStatus: "blocked"` 로 표시하고, `selPkupStr` 기반 `pickupEligibility`(픽업 가능 여부)만 보조 정보로 제공한다.
173
+ - **package 없이 표면을 직접 호출하면 조용히 틀린다.** `SearchGoods`는 검색어 파라미터명이 틀려도 400이 아니라 200에 전체 카탈로그를 돌려준다. 검색어는 JSON 바디가 아니라 쿼리스트링 `searchTerm`으로 보내야 하고, `selStr`도 바디 키가 틀리면 무관한 매장 1건만 준다. 둘 다 실패가 성공처럼 보이므로 반드시 package를 경유한다.
164
174
 
165
175
  ## Notes
166
176
 
@@ -48,7 +48,8 @@ npx -y @nomadamas/k-skill@0 exec fsc-corporate-info scripts/fsc_corporate_info.p
48
48
  - `400 bad_request`: 법인명을 주지 않음.
49
49
  - `503 upstream_not_configured`: 프록시 서버에 `DATA_GO_KR_API_KEY` 없음.
50
50
  - `502 upstream_forbidden`: 프록시 키가 15043184에 활용신청되지 않음.
51
- - 결과: 법인명 불일치 표기를 바꿔 재시도.
51
+ - `coverage`: 기업기본정보 데이터셋 범위, 법인명 후보 선택적 사업자번호 교차검증 기준, 제외 범위, 0건의 의미, 조회시각(`checked_at`)을 구조화해 제공한다.
52
+ - 빈 결과: 이 데이터셋에서 입력 법인명 후보가 없음. 법인이 존재하지 않는다는 뜻이 아니며 표기 차이 가능성이 있다.
52
53
 
53
54
  ## Official surfaces
54
55
 
@@ -0,0 +1,42 @@
1
+ import unittest
2
+
3
+ import fsc_corporate_info as subject
4
+
5
+
6
+ class CoveragePassthroughTest(unittest.TestCase):
7
+ def test_zero_result_keeps_proxy_coverage(self):
8
+ payload = {
9
+ "candidate_count": 0,
10
+ "candidates": [],
11
+ "coverage": {"scope": "fsc-corporate-outline-dataset"},
12
+ }
13
+
14
+ response = subject.query_corp_outline("없는법인", read_json=lambda _request: payload)
15
+
16
+ self.assertIs(response, payload)
17
+ self.assertEqual(response["coverage"]["scope"], "fsc-corporate-outline-dataset")
18
+
19
+ def test_matched_result_keeps_proxy_coverage(self):
20
+ payload = {
21
+ "candidate_count": 1,
22
+ "candidates": [{"corpNm": "테스트"}],
23
+ "coverage": {
24
+ "match_basis": "corporate-name-candidates-with-optional-business-number-cross-check"
25
+ },
26
+ }
27
+
28
+ response = subject.query_corp_outline(
29
+ "테스트",
30
+ "123-45-67890",
31
+ read_json=lambda _request: payload,
32
+ )
33
+
34
+ self.assertIs(response, payload)
35
+ self.assertEqual(
36
+ response["coverage"]["match_basis"],
37
+ "corporate-name-candidates-with-optional-business-number-cross-check",
38
+ )
39
+
40
+
41
+ if __name__ == "__main__":
42
+ unittest.main()
@@ -51,7 +51,8 @@ npx -y @nomadamas/k-skill@0 exec g2b-sanctioned-supplier scripts/g2b_sanctioned_
51
51
  - `400 bad_request`: 사업자번호가 10자리가 아님.
52
52
  - `503 upstream_not_configured`: 프록시 서버에 `DATA_GO_KR_API_KEY` 없음.
53
53
  - `502 upstream_forbidden`: 프록시 키가 15129466에 활용신청되지 않음.
54
- - `total_count = 0`: 조회시점 현재 유효한 제재 없음 (만료·미등록업체는 미제공임에 유의).
54
+ - `coverage`: 현재 유효 제재 범위, 사업자번호 정확 일치 기준, 과거·미등록 제외 범위, 0건의 의미, 조회시각(`checked_at`)을 구조화해 제공한다.
55
+ - `total_count = 0`: 조회시점 현재 유효한 제재가 조회되지 않음. 만료·해제된 과거 제재가 없다는 뜻은 아니다.
55
56
 
56
57
  ## Official surfaces
57
58
 
@@ -0,0 +1,33 @@
1
+ import unittest
2
+
3
+ import g2b_sanctioned_supplier as subject
4
+
5
+
6
+ class CoveragePassthroughTest(unittest.TestCase):
7
+ def test_zero_result_keeps_proxy_coverage(self):
8
+ payload = {
9
+ "total_count": 0,
10
+ "active_sanctions": [],
11
+ "coverage": {"scope": "currently-effective-g2b-sanctions"},
12
+ }
13
+
14
+ response = subject.query_sanctions("123-45-67890", read_json=lambda _request: payload)
15
+
16
+ self.assertIs(response, payload)
17
+ self.assertEqual(response["coverage"]["scope"], "currently-effective-g2b-sanctions")
18
+
19
+ def test_matched_result_keeps_proxy_coverage(self):
20
+ payload = {
21
+ "total_count": 1,
22
+ "active_sanctions": [{"bizno": "1234567890"}],
23
+ "coverage": {"match_basis": "exact-business-number"},
24
+ }
25
+
26
+ response = subject.query_sanctions("1234567890", read_json=lambda _request: payload)
27
+
28
+ self.assertIs(response, payload)
29
+ self.assertEqual(response["coverage"]["match_basis"], "exact-business-number")
30
+
31
+
32
+ if __name__ == "__main__":
33
+ unittest.main()
@@ -27,7 +27,15 @@
27
27
 
28
28
  - 인터넷 연결
29
29
  - `node` 18+
30
- - 이 저장소의 `market-kurly-search` package 또는 동일 로직
30
+ - `market-kurly-search` npm package
31
+
32
+ 설치:
33
+
34
+ ```bash
35
+ npm install market-kurly-search
36
+ ```
37
+
38
+ 이 저장소에서 개발할 때는 루트에서 `npm install` 후 `packages/market-kurly-search`를 쓴다.
31
39
 
32
40
  ## Required inputs
33
41
 
@@ -47,7 +47,8 @@ npx -y @nomadamas/k-skill@0 exec nts-tax-delinquency scripts/nts_tax_delinquency
47
47
  ## Failure modes
48
48
 
49
49
  - `unavailable` + 안내: 상호 미입력, 네트워크 오류, 페이지 구조 변경 추정 — 수동 확인 URL 제공.
50
- - 0건: 명단 모두 매치 없음 (`match_count: 0`).
50
+ - `coverage`: 조회한 공개 명단 범위, 상호·법인명 문자열 대조 기준, 제외 범위, 0건의 의미, 조회시각(`checked_at`)을 구조화해 제공한다.
51
+ - 0건: 두 명단 모두 공개 명단에서 문자열 매치가 없음 (`match_count: 0`). 모든 국세 체납이 없다는 뜻은 아니다.
51
52
 
52
53
  ## Official surfaces
53
54
 
@@ -36,6 +36,18 @@ INDIV_COLUMNS = ("no", "공개년도", "성명", "연령", "상호", "직업(업
36
36
  IDENTITY_NOTE = ("명단공개 자료에는 사업자등록번호가 수록되지 않아 입력 사업자번호와의 "
37
37
  "동일성은 확인할 수 없다 — 상호·법인명 문자열 일치 후보의 공개 사실만 "
38
38
  "나열하며, 동명 상호일 가능성은 사용자가 판단한다.")
39
+ COVERAGE = {
40
+ "scope": "nts-high-amount-habitual-delinquent-disclosure",
41
+ "match_basis": "corporate-name-and-trade-name-string-match",
42
+ "exclusions": [
43
+ "명단공개 기준에 들지 않는 체납 및 비공개 체납",
44
+ "사업자등록번호 동일성 확인",
45
+ ],
46
+ "zero_result_meaning": (
47
+ "조회한 국세청 고액·상습체납자 공개 명단에서 법인명·상호 문자열 일치가 없다는 뜻이며, "
48
+ "모든 국세 체납이 없다는 뜻은 아니다."
49
+ ),
50
+ }
39
51
 
40
52
  _HEADING_MARKER = "고액상습체납자"
41
53
  _ZERO_MARKER = "조회된 데이터가 없습니다"
@@ -50,13 +62,15 @@ def _now_iso() -> str:
50
62
 
51
63
 
52
64
  def _envelope(status: str, *, result: dict | None = None, note: str | None = None) -> dict:
65
+ looked_up_at = _now_iso()
53
66
  return {
54
67
  "source": SOURCE,
55
- "looked_up_at": _now_iso(),
68
+ "looked_up_at": looked_up_at,
56
69
  "status": status,
57
70
  "result": result,
58
71
  "origin": "unauthenticated-public",
59
72
  "note": note,
73
+ "coverage": {**COVERAGE, "checked_at": looked_up_at} if status == "ok" else None,
60
74
  }
61
75
 
62
76
 
@@ -0,0 +1,32 @@
1
+ import unittest
2
+ from unittest.mock import patch
3
+
4
+ import nts_tax_delinquency as subject
5
+
6
+
7
+ class CoverageTest(unittest.TestCase):
8
+ def test_zero_result_explains_disclosure_scope(self):
9
+ with patch.object(subject, "_search", side_effect=[[], []]):
10
+ response = subject.lookup("테스트상사")
11
+
12
+ self.assertEqual(response["status"], "ok")
13
+ self.assertEqual(response["result"]["corporate_list"]["match_count"], 0)
14
+ self.assertEqual(response["result"]["individual_list"]["match_count"], 0)
15
+ self.assertEqual(response["coverage"]["match_basis"], "corporate-name-and-trade-name-string-match")
16
+ self.assertTrue(response["coverage"]["zero_result_meaning"])
17
+ self.assertTrue(response["coverage"]["checked_at"])
18
+
19
+ def test_matched_result_keeps_same_coverage_contract(self):
20
+ corporate = [{"법인명": "테스트상사", "총체납액": "1억원"}]
21
+ individual = [{"상호": "테스트상사", "총체납액": "2억원"}]
22
+ with patch.object(subject, "_search", side_effect=[corporate, individual]):
23
+ response = subject.lookup("테스트상사")
24
+
25
+ self.assertEqual(response["result"]["corporate_list"]["matches"], corporate)
26
+ self.assertEqual(response["result"]["individual_list"]["matches"], individual)
27
+ self.assertEqual(response["coverage"]["scope"], "nts-high-amount-habitual-delinquent-disclosure")
28
+ self.assertIn("사업자등록번호 동일성 확인", response["coverage"]["exclusions"])
29
+
30
+
31
+ if __name__ == "__main__":
32
+ unittest.main()
@@ -1,146 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Bootstrap and run retention-corp/coupang_partners Coupang MCP tools.
3
-
4
- The k-skill repo intentionally does not vendor the third-party implementation.
5
- This wrapper keeps the skill pointed at the approved upstream repository, clones it
6
- into a user cache when needed, and then delegates to its local MCP-compatible CLI.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import argparse
12
- import os
13
- import pathlib
14
- import subprocess
15
- import sys
16
- from typing import Sequence
17
-
18
- UPSTREAM_REPO_URL = "https://github.com/retention-corp/coupang_partners.git"
19
- DEFAULT_MCP_ENDPOINT = "local://coupang-mcp"
20
- DEFAULT_REPO_DIR = pathlib.Path(os.getenv("COUPANG_PARTNERS_REPO_DIR", "~/.cache/k-skill/coupang_partners")).expanduser()
21
- UPSTREAM_CLI = pathlib.Path("bin") / "coupang_mcp.py"
22
-
23
-
24
- class BootstrapError(RuntimeError):
25
- """Raised when the upstream checkout cannot be prepared."""
26
-
27
-
28
- def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
29
- parser = argparse.ArgumentParser(
30
- description="Run the retention-corp/coupang_partners local Coupang MCP-compatible CLI.",
31
- formatter_class=argparse.RawDescriptionHelpFormatter,
32
- epilog=(
33
- "Examples:\n"
34
- " coupang_partners_mcp.py tools\n"
35
- " coupang_partners_mcp.py init\n"
36
- " coupang_partners_mcp.py search 생수\n"
37
- " coupang_partners_mcp.py budget 키보드 --max-price 100000\n"
38
- "\n"
39
- "Honored upstream environment variables (forwarded as-is):\n"
40
- " COUPANG_ACCESS_KEY, COUPANG_SECRET_KEY\n"
41
- " Operator Coupang Partners API credentials. When set, upstream\n"
42
- " uses the local HMAC-signed Coupang Partners path.\n"
43
- " OPENCLAW_SHOPPING_CLIENT_ID\n"
44
- " Allowlisted client id for the hosted fallback. upstream sends\n"
45
- " openclaw-skill by default, which is the value currently on the\n"
46
- " Retention Corp allowlist; k-skill does not override this.\n"
47
- " OPENCLAW_SHOPPING_FORCE_HOSTED=1\n"
48
- " Force the hosted fallback even when Coupang keys are present.\n"
49
- " OPENCLAW_SHOPPING_BASE_URL\n"
50
- " Override the hosted backend base URL. Default upstream target\n"
51
- " is https://a.retn.kr and /v1/public/assist is the public entry.\n"
52
- "\n"
53
- "When both COUPANG_ACCESS_KEY and COUPANG_SECRET_KEY are missing,\n"
54
- "upstream falls back to the hosted Retention Corp backend so this\n"
55
- "skill keeps working without Coupang Partners credentials."
56
- ),
57
- )
58
- parser.add_argument(
59
- "--repo-dir",
60
- default=str(DEFAULT_REPO_DIR),
61
- help="Checkout directory for retention-corp/coupang_partners (default: %(default)s).",
62
- )
63
- parser.add_argument(
64
- "--no-clone",
65
- action="store_true",
66
- help="Do not clone the upstream repository if it is missing; fail with setup guidance instead.",
67
- )
68
- parser.add_argument(
69
- "--update",
70
- action="store_true",
71
- help="Run git pull --ff-only in an existing upstream checkout before delegating.",
72
- )
73
- parser.add_argument(
74
- "upstream_args",
75
- nargs=argparse.REMAINDER,
76
- help="Arguments passed to bin/coupang_mcp.py, for example: tools, search 생수, rocket 에어팟.",
77
- )
78
- args = parser.parse_args(argv)
79
- if args.upstream_args and args.upstream_args[0] == "--":
80
- args.upstream_args = args.upstream_args[1:]
81
- if not args.upstream_args:
82
- parser.error("missing upstream command; try: tools, init, search <keyword>, rocket <keyword>, budget <keyword>")
83
- return args
84
-
85
-
86
- def upstream_cli_path(repo_dir: pathlib.Path) -> pathlib.Path:
87
- return repo_dir / UPSTREAM_CLI
88
-
89
-
90
- def ensure_repo(repo_dir: pathlib.Path, *, clone: bool = True, update: bool = False) -> pathlib.Path:
91
- cli_path = upstream_cli_path(repo_dir)
92
- if cli_path.exists():
93
- if update:
94
- run_checked(["git", "-C", str(repo_dir), "pull", "--ff-only"], "failed to update upstream checkout")
95
- return cli_path
96
-
97
- if repo_dir.exists():
98
- raise BootstrapError(
99
- f"{repo_dir} exists but does not look like retention-corp/coupang_partners "
100
- f"(missing {UPSTREAM_CLI}). Recreate it with: git clone {UPSTREAM_REPO_URL} {repo_dir}"
101
- )
102
-
103
- if not clone:
104
- raise BootstrapError(
105
- f"Missing retention-corp/coupang_partners checkout at {repo_dir}. "
106
- f"Create it with: git clone {UPSTREAM_REPO_URL} {repo_dir}"
107
- )
108
-
109
- repo_dir.parent.mkdir(parents=True, exist_ok=True)
110
- run_checked(["git", "clone", "--depth", "1", UPSTREAM_REPO_URL, str(repo_dir)], "failed to clone upstream checkout")
111
- if not cli_path.exists():
112
- raise BootstrapError(f"Cloned {UPSTREAM_REPO_URL}, but {UPSTREAM_CLI} was not found in {repo_dir}")
113
- return cli_path
114
-
115
-
116
- def run_checked(command: Sequence[str], context: str) -> None:
117
- try:
118
- subprocess.run(command, check=True)
119
- except FileNotFoundError as exc:
120
- raise BootstrapError(f"{context}: required executable not found: {command[0]}") from exc
121
- except subprocess.CalledProcessError as exc:
122
- raise BootstrapError(f"{context}: {exc}") from exc
123
-
124
-
125
- def build_command(cli_path: pathlib.Path, upstream_args: Sequence[str]) -> list[str]:
126
- return [sys.executable, str(cli_path), *upstream_args]
127
-
128
-
129
- def main(argv: Sequence[str] | None = None) -> int:
130
- args = parse_args(argv)
131
- repo_dir = pathlib.Path(args.repo_dir).expanduser().resolve()
132
-
133
- try:
134
- cli_path = ensure_repo(repo_dir, clone=not args.no_clone, update=args.update)
135
- except BootstrapError as exc:
136
- print(f"coupang_partners_mcp.py: {exc}", file=sys.stderr)
137
- return 2
138
-
139
- env = os.environ.copy()
140
- env.setdefault("COUPANG_MCP_ENDPOINT", DEFAULT_MCP_ENDPOINT)
141
- completed = subprocess.run(build_command(cli_path, args.upstream_args), env=env)
142
- return int(completed.returncode)
143
-
144
-
145
- if __name__ == "__main__":
146
- raise SystemExit(main())