@nomadamas/k-skill 0.2.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/package.json +1 -1
  2. package/skills/bok-ecos-stats/instruction.md +1 -1
  3. package/skills/bunjang-search/references/DISCLAIMER.md +23 -0
  4. package/skills/coupang-product-search/references/DISCLAIMER.md +23 -0
  5. package/skills/daangn-cars-search/references/DISCLAIMER.md +23 -0
  6. package/skills/daangn-jobs-search/references/DISCLAIMER.md +23 -0
  7. package/skills/daangn-realty-search/references/DISCLAIMER.md +23 -0
  8. package/skills/daangn-used-goods-search/references/DISCLAIMER.md +23 -0
  9. package/skills/daishin-report-search/references/DISCLAIMER.md +23 -0
  10. package/skills/daiso-product-search/references/DISCLAIMER.md +23 -0
  11. package/skills/danawa-price-search/references/DISCLAIMER.md +23 -0
  12. package/skills/delivery-tracking/references/DISCLAIMER.md +23 -0
  13. package/skills/express-bus-booking/references/DISCLAIMER.md +23 -0
  14. package/skills/fine-dust-location/skill.json +1 -7
  15. package/skills/flight-ticket-search/references/DISCLAIMER.md +23 -0
  16. package/skills/foresttrip-vacancy/references/DISCLAIMER.md +23 -0
  17. package/skills/intercity-bus-booking/references/DISCLAIMER.md +23 -0
  18. package/skills/job-posting-match/references/DISCLAIMER.md +23 -0
  19. package/skills/jobkorea-talent-search/references/DISCLAIMER.md +23 -0
  20. package/skills/k-skill-setup/skill.json +1 -7
  21. package/skills/kakao-bar-nearby/references/DISCLAIMER.md +15 -0
  22. package/skills/kakao-map/references/DISCLAIMER.md +15 -0
  23. package/skills/kakaotalk-mac/references/DISCLAIMER.md +15 -0
  24. package/skills/kbl-results/references/DISCLAIMER.md +13 -0
  25. package/skills/kbo-results/references/DISCLAIMER.md +13 -0
  26. package/skills/keris-academic-search/references/DISCLAIMER.md +13 -0
  27. package/skills/keris-academic-search/references/TRADEMARK-LEGAL-STATEMENT.md +7 -0
  28. package/skills/kleague-results/references/DISCLAIMER.md +13 -0
  29. package/skills/kopis-performance-search/references/DISCLAIMER.md +13 -0
  30. package/skills/kopis-performance-search/references/TRADEMARK-LEGAL-STATEMENT.md +7 -0
  31. package/skills/kopis-performance-search/skill.json +3 -4
  32. package/skills/korean-cinema-search/references/DISCLAIMER.md +13 -0
  33. package/skills/korean-stock-search/references/DISCLAIMER.md +14 -0
  34. package/skills/korean-stock-search/references/TRADEMARK-LEGAL-STATEMENT.md +7 -0
  35. package/skills/korean-transit-route/references/DISCLAIMER.md +13 -0
  36. package/skills/ktx-booking/instruction.md +38 -221
  37. package/skills/ktx-booking/references/AUTOMATION-LEGAL-STATEMENT.md +10 -0
  38. package/skills/ktx-booking/references/DISCLAIMER.md +14 -0
  39. package/skills/ktx-booking/scripts/ktx_booking.py +176 -1274
  40. package/skills/ktx-booking/scripts/ktx_booking.py.lock +27 -0
  41. package/skills/ktx-booking/scripts/ktx_timetable.py +141 -0
  42. package/skills/ktx-booking/skill.json +3 -11
  43. package/skills/lck-analytics/references/DISCLAIMER.md +13 -0
  44. package/skills/lotto-results/references/DISCLAIMER.md +13 -0
  45. package/skills/market-kurly-search/references/DISCLAIMER.md +13 -0
  46. package/skills/myrealtrip-search/instruction.md +3 -1
  47. package/skills/myrealtrip-search/references/DISCLAIMER.md +13 -0
  48. package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +30 -8
  49. package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +62 -0
  50. package/skills/naver-ad-performance/references/DISCLAIMER.md +13 -0
  51. package/skills/naver-blog-research/references/DISCLAIMER.md +13 -0
  52. package/skills/naver-news-search/references/DISCLAIMER.md +13 -0
  53. package/skills/naver-shopping-search/references/DISCLAIMER.md +13 -0
  54. package/skills/ohou-today-deal/references/DISCLAIMER.md +13 -0
  55. package/skills/olive-young-search/references/DISCLAIMER.md +13 -0
  56. package/skills/popbill/references/DISCLAIMER.md +13 -0
  57. package/skills/saramin-talent-search/references/DISCLAIMER.md +13 -0
  58. package/skills/seoul-weather-risk/instruction.md +28 -35
  59. package/skills/seoul-weather-risk/scripts/seoul_weather_risk.py +15 -68
  60. package/skills/seoul-weather-risk/skill.json +2 -2
  61. package/skills/srt-booking/instruction.md +48 -154
  62. package/skills/srt-booking/references/AUTOMATION-LEGAL-STATEMENT.md +25 -0
  63. package/skills/srt-booking/references/DISCLAIMER.md +13 -0
  64. package/skills/srt-booking/scripts/srt_booking.py +134 -235
  65. package/skills/srt-booking/scripts/srt_booking.py.lock +207 -0
  66. package/skills/srt-booking/skill.json +3 -15
  67. package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
  68. package/skills/ticket-availability/references/DISCLAIMER.md +13 -0
  69. package/skills/{toss-securities → toss-investment}/instruction.md +1 -1
  70. package/skills/toss-investment/references/DISCLAIMER.md +14 -0
  71. package/skills/{toss-securities → toss-investment}/references/TRADEMARK-LEGAL-STATEMENT.md +1 -1
  72. package/skills/{toss-securities → toss-investment}/skill.json +2 -2
  73. package/src/assemble.js +1 -1
  74. package/src/execute.js +13 -3
  75. package/skills/lovebug-report/instruction.md +0 -185
  76. package/skills/lovebug-report/skill.json +0 -9
  77. package/skills/srt-booking/scripts/srt_seats.py +0 -156
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What this skill does
4
4
 
5
- 서울 행정동 이름을 정규 `place_id`로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대 단일 제품(`weather_place_risk_window`)을 읽기 전용으로 탐색한다. 기본 helper는 hosted `k-skill-proxy`를 호출하며, 사용자 API Key 발급받거나 저장하지 않는다. 등록 로컬 검증에는 명시적으로 opt-in한 local-direct 경로를 사용할 수 있다. 실패 또는 미준비 상태를 fixture나 추정값으로 대체하지 않는다.
5
+ 서울 행정동 이름을 정규 `place_id`로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대 단일 제품(`weather_place_risk_window`)을 읽기 전용으로 탐색한다. 기본 helper는 hosted `k-skill-proxy`만 호출하며, 사용자 API Key 현재 작업 폴더의 `.env`를 읽지 않는다. 실패 또는 미준비 상태를 fixture나 추정값으로 대체하지 않는다.
6
6
 
7
7
  ## Product
8
8
 
@@ -26,58 +26,52 @@
26
26
 
27
27
  ## Workflow
28
28
 
29
- 1. 환경 설정만 먼저 확인한다. 이 명령은 네트워크를 호출하지 않는다.
29
+ ### Standard user query (fast path)
30
30
 
31
- ```bash
32
- npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- preflight
33
- ```
34
-
35
- 2. bundle에서 이 제품의 준비 상태를 확인한다.
36
-
37
- ```bash
38
- npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- catalog
39
- ```
40
-
41
- 3. 제품의 grain, 기본키, 시간축, 공개 column 및 증거 metadata를 확인한다.
42
-
43
- ```bash
44
- npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- describe --product-id weather_place_risk_window
45
- ```
46
-
47
- 4. 행정동 이름과 `1..500` 범위의 limit으로 data page를 조회한다.
31
+ 사용자가 오늘 위험 시간대를 묻는 기본 경로는 `query --fast` 한 번만 실행한다. 이 경로는 bundled 행정동 매핑과 날짜·limit 검증을 유지하면서 hosted data route만 한 번 호출하므로 bundle·product metadata 왕복을 생략한다. `--fast`에서는 `--filter`를 사용하지 않고 `--admin-dong`, `--gu`, 날짜, `--limit`, `--cursor`만 사용한다.
48
32
 
49
33
  ```bash
50
- npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- query \
34
+ npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- query --fast \
51
35
  --product-id weather_place_risk_window \
52
36
  --admin-dong 잠실본동 \
53
- --from 2026-08-01 \
54
- --to 2026-08-07 \
37
+ --from 2026-08-12 \
38
+ --to 2026-08-12 \
55
39
  --limit 100
56
40
  ```
57
41
 
58
- 동명이명인 `신사동`은 자치구를 확인한 뒤 다음처럼 조회한다.
42
+ 동명이명인 `신사동`은 자치구를 확인한 뒤 fast path에도 `--gu`를 함께 전달한다.
59
43
 
60
44
  ```bash
61
- npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- query \
45
+ npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- query --fast \
62
46
  --product-id weather_place_risk_window \
63
47
  --admin-dong 신사동 \
64
48
  --gu 강남구 \
65
49
  --limit 100
66
50
  ```
67
51
 
68
- 응답의 `registration_ready`, `publication_id`, `blockers`를 먼저 확인한다. data 응답의 `next_cursor`는 같은 제품의 다음 page에만 그대로 재사용한다. publication이 바뀌면 cursor는 `409`로 만료된다.
52
+ `--filter`가 필요하거나 게시 계약을 점검해야 할 때는 `--fast`를 빼고 full-contract query를 사용한다. fast query가 `product_not_ready` 또는 계약 오류를 반환하면 fixture나 추정값으로 대체하지 말고 아래 진단 흐름을 수행한다.
53
+
54
+ ### Contract diagnostics (only when needed)
69
55
 
70
- ## Local pre-registration fallback
56
+ 1. 환경 설정만 확인한다. 이 명령은 네트워크를 호출하지 않는다.
57
+
58
+ ```bash
59
+ npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- preflight
60
+ ```
71
61
 
72
- hosted proxy가 아직 등록·배포되지 않은 상태에서 로컬 검증이 필요할 때만, **현재 작업 디렉터리**의 `.env`에 다음 세 이름을 설정한다.
62
+ 2. bundle에서 제품의 준비 상태를 확인한다.
73
63
 
74
- ```text
75
- KSKILL_LOCAL_DIRECT=1
76
- ASK_SEOUL_SKILL_API_BASE_URL=https://ask-seoul.kr
77
- MARKETPLACE_API_KEY=<기존 로컬 Marketplace 키>
64
+ ```bash
65
+ npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- catalog
66
+ ```
67
+
68
+ 3. 제품의 grain, 기본키, 시간축, 공개 column 및 증거 metadata를 확인한다.
69
+
70
+ ```bash
71
+ npx -y @nomadamas/k-skill@0 exec seoul-weather-risk scripts/seoul_weather_risk.py -- describe --product-id weather_place_risk_window
78
72
  ```
79
73
 
80
- opt-in 모드에서는 helper가 `.env`에서 세 이름만 메모리로 읽고 `Authorization: Bearer`로 전달한다. bundle, 단일 `weather_place_risk_window` product, data 직접 경로 이외에는 호출하지 않는다. 값은 명령행 인수·출력·로그·skill 파일에 넣지 않는다. `KSKILL_LOCAL_DIRECT`가 없으면 기존 hosted-proxy 경로를 그대로 사용한다.
74
+ 진단 응답의 `registration_ready`, `publication_id`, `blockers`를 확인한다. fast/full data 응답의 `next_cursor`는 같은 제품의 다음 page에만 그대로 재사용하며 publication이 바뀌면 cursor는 `409`로 만료된다.
81
75
 
82
76
  ## Boundaries
83
77
 
@@ -85,9 +79,9 @@ MARKETPLACE_API_KEY=<기존 로컬 Marketplace 키>
85
79
  - 알 수 없는 제품이나 필터를 추측해 보정하지 않는다.
86
80
  - 행정동 이름을 fuzzy match하거나 모호한 후보 중 하나로 임의 선택하지 않는다. 생활권·통칭 또는 부분 이름(예: `성수동`)도 행정동으로 추측하지 않는다. helper는 로컬 reference에서 `place_id`를 해석하고 proxy에는 행정동·자치구 문자열을 보내지 않는다.
87
81
  - 기본 proxy origin은 `https://k-skill-proxy.nomadamas.org`이다. 별도 self-host proxy를 쓸 때만 `KSKILL_PROXY_BASE_URL`을 HTTPS origin으로 설정한다. 값은 명령행 인수, 문서, 로그에 넣지 않는다.
88
- - hosted-proxy 모드에서는 사용자 API Key와 `Authorization` 헤더를 사용하지 않는다. ASK Seoul 전용 서비스 키는 proxy 운영 환경에만 두며, Marketplace의 `k-skill-proxy:seoul-weather-risk` principal에 `skill:seoul-weather-risk:read` scope로 등록한다. 이 scope는 bundle·product·data 읽기만 허용하고 다른 Marketplace API를 거부한다. local-direct 모드에서만 현재 작업 폴더 `.env`의 `MARKETPLACE_API_KEY`를 세 direct read 경로에 전달하며, 어떤 모드에서도 키를 출력·로그·skill 파일에 넣지 않는다.
82
+ - hosted-proxy 모드에서는 사용자 API Key와 `Authorization` 헤더를 사용하지 않는다. ASK Seoul 전용 서비스 키는 proxy 운영 환경에만 두며, Marketplace의 `k-skill-proxy:seoul-weather-risk` principal에 `skill:seoul-weather-risk:read` scope로 등록한다. 이 scope는 bundle·product·data 읽기만 허용하고 다른 Marketplace API를 거부한다. 어떤 모드에서도 키를 출력·로그·skill 파일에 넣지 않는다.
89
83
  - proxy는 bundle, 단일 product, 그 data 조회만 노출한다. `table name`, SQL, join, sort, aggregate 및 비허용 query field는 upstream으로 전달하지 않는다.
90
- - `/skill/v1/bundles/seoul-weather-risk`의 제품 집합이 `weather_place_risk_window` 단일 제품과 다르면 응답 계약 오류로 중단한다.
84
+ - `/v1/ask-seoul/weather-risk/bundle`의 제품 집합이 `weather_place_risk_window` 단일 제품과 다르면 응답 계약 오류로 중단한다.
91
85
  - live 실패를 fixture나 synthetic 결과로 대체하지 않는다.
92
86
  - 이 제품은 예보값 임계치 기반 참고 정보이며 기상청 공식 특보를 대체하지 않는다는 점을 응답에서 명확히 한다.
93
87
 
@@ -104,7 +98,6 @@ MARKETPLACE_API_KEY=<기존 로컬 Marketplace 키>
104
98
  - `ambiguous_admin_dong`: 동명이거나 별칭 후보가 충돌해 `--gu`가 필요함. `details.candidates`에서 가능한 자치구를 확인한다.
105
99
  - `location_mapping_invalid`: bundled 행정동 reference의 버전·스키마·행 수 계약 오류
106
100
  - `proxy_disabled`, `invalid_proxy_base_url`: proxy 환경 설정 오류
107
- - `local_direct_not_configured`, `invalid_local_direct_base_url`: local-direct 환경 설정 오류
108
101
  - `unauthorized`/`api_key_missing`(401), `forbidden`/`api_key_forbidden`(403), `unknown_product`(404)
109
102
  - `cursor_expired`(409), `rate_limited`(429), `product_not_ready`(503)
110
103
  - `upstream_not_configured`(503): proxy 운영 환경에 ASK Seoul 전용 서비스 키 또는 origin이 설정되지 않음
@@ -20,16 +20,7 @@ from urllib.request import HTTPRedirectHandler, Request, build_opener
20
20
  SKILL_BUNDLE_ID = "seoul-weather-risk"
21
21
  PROXY_BASE_URL_ENV = "KSKILL_PROXY_BASE_URL"
22
22
  DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
23
- LOCAL_DIRECT_ENV = "KSKILL_LOCAL_DIRECT"
24
- SKILL_API_BASE_URL_ENV = "ASK_SEOUL_SKILL_API_BASE_URL"
25
- MARKETPLACE_API_KEY_ENV = "MARKETPLACE_API_KEY"
26
23
  PROXY_DISABLED_VALUES = frozenset({"off", "false", "0", "disable", "disabled", "none"})
27
- LOCAL_DIRECT_ENABLED_VALUES = frozenset({"1", "true", "on", "yes"})
28
- LOCAL_DIRECT_DOTENV_NAMES = frozenset({
29
- LOCAL_DIRECT_ENV,
30
- SKILL_API_BASE_URL_ENV,
31
- MARKETPLACE_API_KEY_ENV,
32
- })
33
24
  PROXY_ROUTE_ROOT = "/v1/ask-seoul/weather-risk"
34
25
  LOCATION_MAPPING_PATH = pathlib.Path(__file__).resolve().parents[1] / "references" / "admin-dong-place-map.json"
35
26
  LOCATION_MAPPING_VERSION = "kma_admin_dong_grid_20260325"
@@ -65,54 +56,14 @@ class _NoRedirect(HTTPRedirectHandler):
65
56
  @dataclass(frozen=True)
66
57
  class ApiConfig:
67
58
  base_url: str
68
- mode: str = "hosted_proxy"
69
- bearer_token: str | None = None
70
59
 
71
60
 
72
61
  def _json(value: Any) -> str:
73
62
  return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
74
63
 
75
64
 
76
- def _local_direct_config(values: dict[str, str]) -> ApiConfig:
77
- base_url = values.get(SKILL_API_BASE_URL_ENV, "").strip()
78
- bearer_token = values.get(MARKETPLACE_API_KEY_ENV, "").strip()
79
- if not base_url or base_url == "replace-me":
80
- raise SkillError("local_direct_not_configured", "local direct API base URL이 필요합니다.")
81
- if not bearer_token or bearer_token == "replace-me":
82
- raise SkillError("local_direct_not_configured", "local direct Marketplace API key가 필요합니다.")
83
-
84
- parsed = urlparse(base_url)
85
- local_http = parsed.scheme == "http" and parsed.hostname in {"127.0.0.1", "localhost", "::1"}
86
- if (parsed.scheme != "https" and not local_http) or not parsed.netloc or parsed.path not in {"", "/"} or parsed.query or parsed.fragment:
87
- raise SkillError("invalid_local_direct_base_url", "local direct API base URL은 HTTPS origin이어야 합니다.")
88
- if parsed.username or parsed.password:
89
- raise SkillError("invalid_local_direct_base_url", "local direct API base URL에 사용자 정보는 포함할 수 없습니다.")
90
- return ApiConfig(base_url=base_url.rstrip("/"), mode="local_direct", bearer_token=bearer_token)
91
-
92
-
93
- def _current_directory_dotenv() -> dict[str, str]:
94
- path = pathlib.Path.cwd() / ".env"
95
- try:
96
- lines = path.read_text(encoding="utf-8").splitlines()
97
- except OSError:
98
- return {}
99
-
100
- values: dict[str, str] = {}
101
- for line in lines:
102
- stripped = line.strip()
103
- if not stripped or stripped.startswith("#") or "=" not in stripped:
104
- continue
105
- name, value = stripped.split("=", 1)
106
- name = name.strip()
107
- if name in LOCAL_DIRECT_DOTENV_NAMES:
108
- values[name] = value.strip().strip('"').strip("'")
109
- return values
110
-
111
-
112
65
  def _api_config(environ: dict[str, str] | None = None) -> ApiConfig:
113
- values = ({**_current_directory_dotenv(), **os.environ} if environ is None else environ)
114
- if values.get(LOCAL_DIRECT_ENV, "").strip().casefold() in LOCAL_DIRECT_ENABLED_VALUES:
115
- return _local_direct_config(values)
66
+ values = os.environ if environ is None else environ
116
67
  configured = values.get(PROXY_BASE_URL_ENV, "").strip()
117
68
  if configured.casefold() in PROXY_DISABLED_VALUES:
118
69
  raise SkillError("proxy_disabled", f"{PROXY_BASE_URL_ENV}가 비활성화되어 있습니다.")
@@ -159,8 +110,6 @@ def _request_json(config: ApiConfig, path: str, query: dict[str, str] | None = N
159
110
  "Accept": "application/json",
160
111
  "User-Agent": "k-skill-seoul-weather-risk/1",
161
112
  }
162
- if config.bearer_token:
163
- headers["Authorization"] = f"Bearer {config.bearer_token}"
164
113
  request = Request(url, headers=headers)
165
114
  try:
166
115
  with build_opener(_NoRedirect).open(request, timeout=15) as response:
@@ -399,22 +348,16 @@ def _validate_product_id(product_id: str) -> None:
399
348
 
400
349
 
401
350
  def _bundle(config: ApiConfig) -> dict[str, Any]:
402
- if config.mode == "local_direct":
403
- return _validate_bundle(_request_json(config, f"/skill/v1/bundles/{SKILL_BUNDLE_ID}"))
404
351
  return _validate_bundle(_request_json(config, f"{PROXY_ROUTE_ROOT}/bundle"))
405
352
 
406
353
 
407
354
  def _detail(config: ApiConfig, product_id: str) -> dict[str, Any]:
408
355
  _validate_product_id(product_id)
409
- if config.mode == "local_direct":
410
- return _validate_product(_request_json(config, f"/skill/v1/products/{product_id}"), product_id)
411
356
  return _validate_product(_request_json(config, f"{PROXY_ROUTE_ROOT}/product"), product_id)
412
357
 
413
358
 
414
359
  def _data(config: ApiConfig, product_id: str, query: dict[str, str], limit: int) -> dict[str, Any]:
415
360
  _validate_product_id(product_id)
416
- if config.mode == "local_direct":
417
- return _validate_data(_request_json(config, f"/skill/v1/products/{product_id}/data", query), product_id, limit)
418
361
  return _validate_data(_request_json(config, f"{PROXY_ROUTE_ROOT}/data", query), product_id, limit)
419
362
 
420
363
 
@@ -428,6 +371,7 @@ def _parser() -> argparse.ArgumentParser:
428
371
  describe.add_argument("--product-id", required=True)
429
372
 
430
373
  query = commands.add_parser("query", help="제품 data page 조회")
374
+ query.add_argument("--fast", action="store_true", help="bundle/product metadata 확인을 생략하고 data만 조회")
431
375
  query.add_argument("--product-id", required=True)
432
376
  query.add_argument("--filter", action="append", default=[], metavar="COLUMN=VALUE")
433
377
  query.add_argument("--admin-dong", help="서울 행정동 이름")
@@ -446,11 +390,9 @@ def run(argv: list[str]) -> int:
446
390
  if args.command == "preflight":
447
391
  result = {
448
392
  "status": "ok",
449
- "mode": config.mode,
393
+ "mode": "hosted_proxy",
450
394
  "live_network": False,
451
- "user_api_key_required": config.mode == "local_direct",
452
- "proxy_base_url_configured": config.mode == "hosted_proxy",
453
- "local_direct_base_url_configured": config.mode == "local_direct",
395
+ "proxy_base_url_configured": True,
454
396
  }
455
397
  elif args.command == "catalog":
456
398
  result = _bundle(config)
@@ -460,8 +402,12 @@ def run(argv: list[str]) -> int:
460
402
  else:
461
403
  if not 1 <= args.limit <= 500:
462
404
  raise SkillError("invalid_limit", "limit은 1부터 500 사이여야 합니다.")
463
- _bundle(config)
464
- detail = _detail(config, args.product_id)
405
+ if args.fast and args.filter:
406
+ raise SkillError("fast_query_filter_unsupported", "--fast에서는 --filter를 사용할 수 없습니다.")
407
+ detail: dict[str, Any] | None = None
408
+ if not args.fast:
409
+ _bundle(config)
410
+ detail = _detail(config, args.product_id)
465
411
  filters = _filters(args.filter)
466
412
  if args.gu is not None and args.admin_dong is None:
467
413
  raise SkillError("invalid_location_input", "--gu는 --admin-dong과 함께 사용해야 합니다.")
@@ -477,10 +423,11 @@ def run(argv: list[str]) -> int:
477
423
  )
478
424
  resolved = _resolve_admin_dong(args.admin_dong, args.gu)
479
425
  filters["place_id"] = resolved["place_id"]
480
- allowed_columns = {column["name"] for column in detail["metadata"].get("columns", [])}
481
- unknown = sorted(set(filters) - allowed_columns)
482
- if unknown:
483
- raise SkillError("unknown_filter", f"공개 projection에 없는 필터입니다: {', '.join(unknown)}")
426
+ if detail is not None:
427
+ allowed_columns = {column["name"] for column in detail["metadata"].get("columns", [])}
428
+ unknown = sorted(set(filters) - allowed_columns)
429
+ if unknown:
430
+ raise SkillError("unknown_filter", f"공개 projection에 없는 필터입니다: {', '.join(unknown)}")
484
431
  request_query = {**filters, "limit": str(args.limit)}
485
432
  if args.from_value is not None:
486
433
  request_query["from"] = _time_bound(args.from_value, "from")
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "seoul-weather-risk",
3
- "description": "서울 행정동 이름을 정식명 우선·허용된 결정적 표기 별칭만으로 정규 place_id로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대(weather_place_risk_window) 단일 제품을 hosted k-skill proxy에서 읽기 전용 조회한다. 기본 경로에는 사용자 API Key와 place_id 입력이 필요 없으며, 등록 전에는 명시적으로 local-direct 검증을 할 수 있다.",
3
+ "description": "서울 행정동 이름을 정식명 우선·허용된 결정적 표기 별칭만으로 정규 place_id로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대(weather_place_risk_window) 단일 제품을 hosted k-skill proxy에서 읽기 전용 조회한다. 사용자 API Key와 place_id 입력은 필요하지 않다.",
4
4
  "profiles": [
5
5
  "proxy",
6
6
  "lookup"
7
7
  ],
8
- "frontmatter": "name: seoul-weather-risk\ndescription: 서울 행정동 이름을 정식명 우선·허용된 결정적 표기 별칭만으로 정규 place_id로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대(weather_place_risk_window) 단일 제품을 hosted k-skill proxy에서 읽기 전용 조회한다. 기본 경로에는 사용자 API Key와 place_id 입력이 필요 없으며, 등록 전에는 명시적으로 local-direct 검증을 할 수 있다.\nlicense: MIT\nmetadata:\n category: public-data\n locale: ko-KR\n phase: live-client"
8
+ "frontmatter": "name: seoul-weather-risk\ndescription: 서울 행정동 이름을 정식명 우선·허용된 결정적 표기 별칭만으로 정규 place_id로 해석해 ASK 서울의 장소별 기상 위험 예상 시간대(weather_place_risk_window) 단일 제품을 hosted k-skill proxy에서 읽기 전용 조회한다. 사용자 API Key와 place_id 입력은 필요하지 않다.\nlicense: MIT\nmetadata:\n category: public-data\n locale: ko-KR\n phase: live-client"
9
9
  }
@@ -1,181 +1,75 @@
1
- # SRT Booking
1
+ # SRT Live Timetable Lookup
2
2
 
3
3
  ## What this skill does
4
4
 
5
- `SRTrain` 위에 `scripts/srt_booking.py` helper 얹어 SRT 조회와 호차별 좌석번호 확인을 처리하고, 예약과 취소는 고정된 열차/예약을 다시 식별한 뒤 `SRTrain`으로 진행한다.
5
+ `SRTrain`의 시간표 검색 경로를 이용해 현재 SRT 운행 후보와 일반실·특실 예약 가능 여부를 조회한다.
6
6
 
7
- ## When to use
7
+ 스킬은 회원 로그인 불필요한 **라이브 조회 전용**이다.
8
8
 
9
- - "수서에서 부산 가는 SRT 찾아줘"
10
- - "내일 오전 SRT 빈자리 있으면 잡아줘"
11
- - "SRT 5호차 좌석 확인해줘"
12
- - "SRT 11A 좌석이 비었는지 봐줘"
13
- - "창측 좌석을 우선해서 SRT 빈자리 보여줘"
14
- - "예약 내역 확인해줘"
15
- - "이 SRT 예약 취소해줘"
9
+ - `auto_login=False` 익명 client를 사용한다.
10
+ - NetFunnel 대기열과 시간표 검색 요청만 실행한다.
11
+ - 예약, 예약대기, 좌석 선점, 결제, 취소, 자동 재조회는 실행하지 않는다.
12
+ - 정확한 호차·좌석번호를 조회하지 않는다.
13
+ - 구매는 공식 SRT 페이지에서 사용자가 직접 진행한다.
16
14
 
17
- ## When not to use
15
+ 실행 환경에는 Python 3.11 이상과 `uv`가 필요하다. `uv`가 없으면 공식 설치 문서를 안내하고 조회를 실행하지 않는다.
18
16
 
19
- - 돌쇠가 아니며 결제까지 자동으로 끝내야 하는 경우
20
- - 비밀번호를 채팅창에 직접 보내려는 경우
21
- - SRT가 아니라 KTX/Korail 예매인 경우
22
-
23
- ## Prerequisites
24
-
25
- - Python 3.10+
26
- - `python3 -m pip install SRTrain`
27
-
28
- ## Required environment variables
29
-
30
- - `KSKILL_SRT_ID`
31
- - `KSKILL_SRT_PASSWORD`
32
-
33
- ### Credential handling
34
-
35
- - 돌쇠 credential mode에서는 `vault-run` capability를 사용하고, 없으면 `request_vault_credential`을 호출한다. ID/PW 원문을 채팅이나 shell에 넣지 않는다.
36
- - 그 밖의 환경에서는 이미 주입된 환경변수 → host vault → `~/.config/k-skill/secrets.env` 순서로 사용한다.
37
-
38
- ## Inputs
39
-
40
- - 출발역
41
- - 도착역
42
- - 날짜: `YYYYMMDD`
43
- - 희망 시작 시각: `HHMMSS`
44
- - 인원 수와 승객 유형
45
- - 좌석 선호: 일반실 / 특실
46
- - 좌석 상세 조건: 객실 등급, 호차 번호, 좌석 번호, 빈 좌석만 보기, 탐색 우선순위
47
-
48
- ## Workflow
49
-
50
- ### 0. Install the package globally when missing
51
-
52
- `python3 -c 'import SRT'` 가 실패하면 다른 구현으로 우회하지 말고 전역 Python 패키지 설치를 먼저 시도한다.
53
-
54
- ```bash
55
- python3 -m pip install SRTrain
56
- ```
57
-
58
- ### 1. Ensure credentials are available
59
-
60
- 돌쇠 credential mode에서는 `vault-run`의 SRT capability를 확인하고, 없으면 `request_vault_credential`로 앱 vault 입력 UI를 호출한다. generic fallback에서만 `KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD` 환경변수를 확인한다.
61
-
62
- 시크릿이 없다는 이유로 웹사이트를 직접 긁거나 다른 비공식 경로를 찾지 않는다.
63
-
64
- ### 2. Search first
65
-
66
- 먼저 helper 로 조회해서 후보를 요약한다.
67
-
68
- ```bash
69
- npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- search 수서 부산 20260328 080000 --time-limit 120000 --limit 5
70
- ```
71
-
72
- ### 3. Summarize options before side effects
73
-
74
- 예약 전에는 항상 아래를 짧게 정리한다.
75
-
76
- - 출발/도착 시각
77
- - 일반실/특실 가능 여부
78
- - 예상 운임
79
-
80
- ### 4. Inspect detailed seats when the user asks for seat numbers
81
-
82
- `search` 의 좌석 가능 여부는 열차 단위 플래그다. 사용자가 "남은 좌석 번호", "호차별 좌석", "특정 좌석", "창측/순방향 자리", "예약 전에 자리 확인"처럼 구체적인 좌석을 물으면 예약 전에 `seats` 를 호출한다.
83
-
84
- ```bash
85
- npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- seats 수서 부산 20260328 080000 --train-id <train_id>
86
- ```
87
-
88
- 특정 호차의 빈 좌석만 확인하려면 `--car-no` 와 `--available-only` 를 쓴다.
17
+ ## Commands
89
18
 
90
19
  ```bash
91
- npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- seats 수서 부산 20260328 080000 --train-id <train_id> --car-no 5 --available-only
20
+ npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- \
21
+ search \
22
+ --dep 수서 \
23
+ --arr 부산 \
24
+ --date 20260819 \
25
+ --time 0600 \
26
+ --time-limit 1200 \
27
+ --limit 5
92
28
  ```
93
29
 
94
- 특정 좌석이 비었는지 확인하려면 `--seat` 를 붙인다.
30
+ 현재 라이브 조회 endpoint와 안전 경계:
95
31
 
96
32
  ```bash
97
- npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- seats 수서 부산 20260328 080000 --train-id <train_id> --car-no 5 --seat 11A
33
+ npx -y @nomadamas/k-skill@0 exec srt-booking scripts/srt_booking.py -- source
98
34
  ```
99
35
 
100
- 특정 호차를 지정하지 않으면 가운데 호차부터 탐색한다. `--car-priority center|low|high` 로 호차 탐색 순서를 바꾸고, `--seat-priority forward-window|window-forward|row-low` 로 좌석 정렬 우선순위를 바꾼다.
101
-
102
- 상세 좌석 응답을 보여줄 때는 아래를 우선 요약한다.
103
-
104
- - 호차별 `available_seat_count`
105
- - 남은 좌석 번호 (`available_seats`)
106
- - 좌석별 `direction`, `position`
107
- - 특정 좌석 요청이면 `requested_seat_available`
108
-
109
- 이 기능은 좌석을 선택/선점하지 않는다. 실제 예약은 다음 단계에서만 진행한다.
110
-
111
- ### 5. Reserve only after the train is fixed
36
+ 출력:
112
37
 
113
- 예약은 부작용이 있으므로 정확한 열차를 고른 뒤에만 진행한다.
38
+ - 열차번호·열차종류
39
+ - 출발역·도착역
40
+ - 현재 출발·도착 시각
41
+ - 일반실·특실 예약 가능 여부
42
+ - 사용한 라이브 search endpoint
43
+ - 공식 SRT 페이지
114
44
 
115
- ```bash
116
- python3 - <<'PY'
117
- import os
118
- from SRT import Adult, SRT, SeatType
119
-
120
- srt = SRT(os.environ["KSKILL_SRT_ID"], os.environ["KSKILL_SRT_PASSWORD"])
121
- trains = srt.search_train("수서", "부산", "20260328", "080000", time_limit="120000")
122
- reservation = srt.reserve(
123
- trains[0],
124
- passengers=[Adult(1)],
125
- special_seat=SeatType.GENERAL_FIRST,
126
- )
127
- print(reservation)
128
- PY
129
- ```
45
+ ## Workflow
130
46
 
131
- ### 6. Continue to payment in Dolshoi
47
+ 1. 출발역, 도착역, 날짜, 시간대를 확인한다.
48
+ 2. `search`를 한 번 실행한다. `수서역`처럼 `역`이 붙은 입력은 helper가 표준 역명으로 정규화한다.
49
+ 3. 현재 운행 후보를 제시한다.
50
+ 4. 좌석 구매가 필요하면 `booking_url`을 제공하고 종료한다.
51
+ 5. 사용자가 다시 요청하지 않는 한 polling·매진 감시를 시작하지 않는다.
132
52
 
133
- 예약이 성공하면 예약번호, 운임, 구입기한을 즉시 알려서 **좌석 확보는 완료되었다**고 명확히 말한다.
53
+ ## Hard boundaries
134
54
 
135
- 돌쇠에서 사용자가 예매 완료를 요청했다면 여기서 멈추지 않는다.
55
+ - `KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD` 또는 회원 로그인을 요구하지 않는다.
56
+ - helper에 `reserve`, `reservations`, `cancel`, `payment`, waiting-list 명령을 추가하지 않는다.
57
+ - 예약 endpoint, 결제 endpoint 또는 계정 endpoint를 호출하지 않는다.
58
+ - NetFunnel key를 예약·선점 자동화에 사용하지 않는다.
59
+ - CAPTCHA·접근 거부·계정 제한이 발생하면 즉시 중단한다.
60
+ - 사용자 요청 한 번을 반복 수집이나 장기 실행으로 확장하지 않는다.
136
61
 
137
- 1. CloakBrowser의 공식 SRT 예약/결제 화면에서 방금 만든 예약을 다시 식별한다.
138
- 2. vault-backed login을 사용하고 결제수단/할인/승객/예약 정보를 확인한다.
139
- 3. 실제 결제 버튼 직전에 `clarify`로 열차, 날짜·시각, 승객, 좌석 등급, 총액을 보여주고 승인받는다.
140
- 4. 승인되면 결제를 실행하고 결제 완료 화면, 예약번호, 영수증/결제 상태를 확인한다.
62
+ ## Failure modes
141
63
 
142
- 돌쇠가 아니거나 공식 결제 표면을 사용할 수 없으면 예약번호와 구입기한을 제공하고 generic handoff로 종료한다.
64
+ - NetFunnel 대기 또는 SRT 접근 제한
65
+ - `SRTrain` 설치·호환성 문제
66
+ - 날짜·시간 또는 역명 오류
67
+ - 조건에 맞는 열차 없음
143
68
 
144
- ### 7. Inspect or cancel
69
+ 경우 차단을 우회하지 않고 [SRT 공식 조회 페이지](https://etk.srail.kr/hpg/hra/01/selectScheduleList.do?pageId=TK0101010000)를 안내한다.
145
70
 
146
- 취소 전에는 대상 예약을 다시 식별한다.
71
+ ## Legal notice
147
72
 
148
73
  ```bash
149
- python3 - <<'PY'
150
- import os
151
- from SRT import SRT
152
-
153
- srt = SRT(os.environ["KSKILL_SRT_ID"], os.environ["KSKILL_SRT_PASSWORD"])
154
- reservations = srt.get_reservations()
155
- print(reservations)
156
- PY
74
+ npx -y @nomadamas/k-skill@0 read srt-booking references/AUTOMATION-LEGAL-STATEMENT.md
157
75
  ```
158
-
159
- 취소 실행 직전에 `clarify`로 예약번호, 열차, 날짜·시각, 승객, 환불/위약금 정보를 확인하고 승인받는다.
160
-
161
- ## Done when
162
-
163
- - 조회 요청이면 후보 열차가 정리되어 있다
164
- - 좌석 상세 확인이면 호차별 남은 좌석번호나 특정 좌석 공석 여부가 정리되어 있다
165
- - 예약 요청이면 예약 결과, 운임, 구입기한이 확인되어 좌석 확보 완료를 안내했다
166
- - 돌쇠의 예매 완료 요청이면 `clarify` 승인 후 결제 완료 상태와 영수증/예약번호를 확인했다
167
- - 취소 요청이면 어떤 예약을 취소했는지 명확하다
168
-
169
- ## Failure modes
170
-
171
- - 로그인 오류: 계정 정보나 SRT site policy 변경 가능성 확인
172
- - 매진: 다른 시간대나 좌석 타입으로 재조회
173
- - 좌석선택 페이지 형식 변경: helper 파서 업데이트 필요
174
- - 네트워크 오류: 짧게 재시도하되 aggressive polling은 피하기
175
-
176
- ## Notes
177
-
178
- - 상세 좌석 확인은 SRT 웹 좌석선택 페이지를 조회 전용으로 읽는다
179
- - `SRTrain`은 SRT 전용 라이브러리라서 스킬 의도가 더 선명하다
180
- - 결제 자동화 금지는 generic fallback에만 적용한다. 돌쇠에서는 `clarify` 승인 후 공식 결제 표면으로 완료한다
181
- - 자동 재시도 루프는 계정 보호 차원에서 짧고 보수적으로 유지한다
@@ -0,0 +1,25 @@
1
+ # SRT 라이브 자동조회 관련 고지
2
+
3
+ > 2026-08-12에 확인한 공개 자료를 바탕으로 한 일반적인 위험 안내이며 법률자문이나 주식회사 에스알의 유권해석이 아니다.
4
+
5
+ ## 결론
6
+
7
+ 이 스킬은 로그인 없이 `SRTrain`의 시간표 검색 endpoint만 호출하고 예약·결제·취소·좌석 선점을 제공하지 않는다. 따라서 기존 자동예매보다 위험은 낮다.
8
+
9
+ 그러나 에스알은 공식 공지에서 매크로 프로그램을 이용한 **승차권 예매**를 금지한다. 조회 전용이라는 사실이 자동 접근에 대한 에스알의 허가를 뜻하지는 않는다.
10
+
11
+ ## 기술적 경계
12
+
13
+ - `SRT("", "", auto_login=False)` 익명 client
14
+ - NetFunnel 대기열과 `selectListAra10007_n.do` 시간표 검색만 실행
15
+ - 일반실·특실 가능 여부만 조회
16
+ - 예약·예약대기·결제·취소·계정 endpoint 없음
17
+ - 한 사용자 요청에 한 번만 실행하고 polling 없음
18
+
19
+ ## 공식 근거
20
+
21
+ [매크로 프로그램을 이용한 승차권 예매 금지 안내](https://etk.srail.kr/cms/article/view.do?pageId=TK0502000000&postNo=793)는 매크로를 이용한 예매가 시스템 과부하와 다른 고객의 예매 기회 방해를 초래할 수 있고 적발 시 회원 탈퇴 처리될 수 있다고 안내한다.
22
+
23
+ [에스알 회원가입 및 이용에 관한 약관](https://etk.srail.kr/cms/archive.do?pageId=TK0503010000) 제10조는 다량 예약 후 임박 취소, 타인 계정 도용과 건전한 서비스 이용 저해를 탈퇴 사유로 규정한다.
24
+
25
+ NetFunnel 오류, 접근 거부, CAPTCHA 또는 정책 변경이 발생하면 우회하지 않고 즉시 중단한다.
@@ -0,0 +1,13 @@
1
+ # DISCLAIMER — `srt-booking`
2
+
3
+ 이 스킬은 에스알·SRT 또는 철도 운영사의 공식 기능 또는 공식 지원 도구가 아니며, 공식 제휴·후원·승인·인증 또는 협업한 사실이 전혀 없습니다. 상표와 서비스명은 열차·좌석 조회 기능과 대상을 설명하기 위해서만 사용합니다.
4
+
5
+ 대법원 2005. 6. 10. 선고 [2005도1637 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=83920)(소니용 리모컨 사건)은 기능 설명용 표장과 출처표시를 구별했습니다. [상표법 제2조](https://www.law.go.kr/법령/상표법/제2조), [제89조](https://www.law.go.kr/법령/상표법/제89조), [제90조](https://www.law.go.kr/법령/상표법/제90조), [제108조](https://www.law.go.kr/법령/상표법/제108조)에 따른 출처 혼동 판단은 별도입니다.
6
+
7
+ 대법원 2022. 5. 12. 선고 [2021도1533 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=221765)은 공개정보 수집만으로 곧바로 [정보통신망법 제48조](https://www.law.go.kr/법령/정보통신망이용촉진및정보보호등에관한법률/제48조) 위반이 되는 것은 아닌 사정을 제시했지만, 철도 예매 자동화·접근통제 우회·반복 선점을 허용하지 않습니다. [저작권법 제93조](https://www.law.go.kr/법령/저작권법/제93조)과 [형법 제314조 제2항](https://www.law.go.kr/법령/형법/제314조)은 별도입니다.
8
+
9
+ - 공개 운행·좌석정보 자동 수집은 반드시 개인의 정보 조회용으로만 사용합니다.
10
+ - 매진 감시, 조직적·대량 크롤링, 반복 선점·예약·취소, 공격적 polling을 하지 않습니다.
11
+ - CAPTCHA·anti-bot·로그인·접근통제·rate limit·IP 차단을 우회하거나 정상 예매 영업을 방해하지 않습니다.
12
+
13
+ 이 문서는 적법성을 보증하는 법률 자문이 아닙니다.