@nomadamas/k-skill 0.2.4 → 0.3.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 (28) hide show
  1. package/package.json +1 -1
  2. package/skills/k-skill-cleaner/instruction.md +1 -1
  3. package/skills/k-skill-setup/instruction.md +114 -157
  4. package/skills/k-skill-setup/skill.json +2 -2
  5. package/skills/ktx-booking/instruction.md +38 -221
  6. package/skills/ktx-booking/references/AUTOMATION-LEGAL-STATEMENT.md +10 -0
  7. package/skills/ktx-booking/scripts/ktx_booking.py +176 -1274
  8. package/skills/ktx-booking/scripts/ktx_booking.py.lock +27 -0
  9. package/skills/ktx-booking/scripts/ktx_timetable.py +141 -0
  10. package/skills/ktx-booking/skill.json +11 -5
  11. package/skills/seoul-weather-risk/instruction.md +28 -35
  12. package/skills/seoul-weather-risk/scripts/seoul_weather_risk.py +15 -68
  13. package/skills/seoul-weather-risk/skill.json +2 -2
  14. package/skills/srt-booking/instruction.md +48 -154
  15. package/skills/srt-booking/references/AUTOMATION-LEGAL-STATEMENT.md +25 -0
  16. package/skills/srt-booking/scripts/srt_booking.py +134 -235
  17. package/skills/srt-booking/scripts/srt_booking.py.lock +207 -0
  18. package/skills/srt-booking/skill.json +5 -7
  19. package/skills/store-longevity-radar/instruction.md +7 -3
  20. package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
  21. package/skills/store-longevity-radar/scripts/store_longevity_download.py +136 -0
  22. package/skills/store-longevity-radar/scripts/store_longevity_radar.py +1 -51
  23. package/src/assemble.js +1 -1
  24. package/src/execute.js +13 -3
  25. package/skills/srt-booking/scripts/srt_seats.py +0 -156
  26. package/skills/used-car-price-search/instruction.md +0 -109
  27. package/skills/used-car-price-search/references/TRADEMARK-LEGAL-STATEMENT.md +0 -9
  28. package/skills/used-car-price-search/skill.json +0 -8
@@ -1,156 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import re
5
- from typing import TypedDict
6
-
7
-
8
- class SrtCar(TypedDict):
9
- car_no: int
10
- car_no_raw: str
11
- room_class: str
12
- available: bool
13
- current: bool
14
-
15
-
16
- class SrtSeat(TypedDict):
17
- seat: str
18
- seat_no: str
19
- available: bool
20
- direction: str
21
- position: str
22
- notes: list[str]
23
-
24
-
25
- CAR_RE = re.compile(
26
- r'<li class="scar-(?P<car>\d+)(?P<class>[^"]*)">(?P<body>.*?)</li>',
27
- re.DOTALL,
28
- )
29
- SEAT_LINK_RE = re.compile(
30
- r"<a[^>]+selectSeatInfo\(this,\s*'(?P<seat_no>[^']+)',\s*'(?P<seat>[^']+)'\)[^>]*>"
31
- r".*?<em>\((?P<detail>[^)]*)\)</em>",
32
- re.DOTALL,
33
- )
34
- SEAT_SPAN_RE = re.compile(
35
- r"<span>\s*(?P<seat>\d+[A-Z])\s*<strong><em>\((?P<detail>[^)]*)\)</em></strong></span>",
36
- re.DOTALL,
37
- )
38
- TAG_RE = re.compile(r"<[^>]+>")
39
-
40
-
41
- def strip_tags(value: str) -> str:
42
- return TAG_RE.sub(" ", value).replace("\xa0", " ").strip()
43
-
44
-
45
- def parse_detail(detail: str) -> tuple[str, str, list[str]]:
46
- parts = [part.strip() for part in detail.split(",")]
47
- direction = next((part for part in parts if part in {"정방향", "역방향"}), "unknown")
48
- position = next((part for part in parts if part in {"창측", "내측", "1인석"}), "unknown")
49
- notes = [part for part in parts if part not in {direction, position} and part]
50
- return direction, position, notes
51
-
52
-
53
- def parse_cars(html: str) -> list[SrtCar]:
54
- cars: list[SrtCar] = []
55
- for match in CAR_RE.finditer(html):
56
- body = match.group("body")
57
- text = strip_tags(body)
58
- room_class = "특실" if "특실" in text else "일반실"
59
- css_class = match.group("class")
60
- has_link = "selectScarInfo" in body
61
- cars.append(
62
- {
63
- "car_no": int(match.group("car")),
64
- "car_no_raw": f"{int(match.group('car')):04d}",
65
- "room_class": room_class,
66
- "available": has_link and "off" not in css_class.split(),
67
- "current": "on" in css_class.split(),
68
- }
69
- )
70
- return cars
71
-
72
-
73
- def parse_seats(html: str) -> list[SrtSeat]:
74
- seats: list[SrtSeat] = []
75
- seen: set[str] = set()
76
- for match in SEAT_LINK_RE.finditer(html):
77
- direction, position, notes = parse_detail(match.group("detail"))
78
- seat = match.group("seat")
79
- seen.add(seat)
80
- seats.append(
81
- {
82
- "seat": seat,
83
- "seat_no": match.group("seat_no"),
84
- "available": True,
85
- "direction": direction,
86
- "position": position,
87
- "notes": notes,
88
- }
89
- )
90
- for match in SEAT_SPAN_RE.finditer(html):
91
- seat = match.group("seat")
92
- if seat in seen:
93
- continue
94
- direction, position, notes = parse_detail(match.group("detail"))
95
- seats.append(
96
- {
97
- "seat": seat,
98
- "seat_no": "",
99
- "available": False,
100
- "direction": direction,
101
- "position": position,
102
- "notes": notes,
103
- }
104
- )
105
- return seats
106
-
107
-
108
- def parse_seat_label(seat_label: str) -> tuple[int | None, str]:
109
- match = re.match(r"^(\d+)([A-Z])$", seat_label)
110
- if match is None:
111
- return None, ""
112
- return int(match.group(1)), match.group(2)
113
-
114
-
115
- def car_center_priority(car: SrtCar, car_numbers: list[int]) -> tuple[float, int]:
116
- if not car_numbers:
117
- return (0.0, car["car_no"])
118
- center = (min(car_numbers) + max(car_numbers)) / 2
119
- return (abs(car["car_no"] - center), car["car_no"])
120
-
121
-
122
- def sort_cars_for_booking(cars: list[SrtCar], priority: str = "center") -> list[SrtCar]:
123
- match priority:
124
- case "center":
125
- car_numbers = [car["car_no"] for car in cars]
126
- return sorted(cars, key=lambda car: car_center_priority(car, car_numbers))
127
- case "low":
128
- return sorted(cars, key=lambda car: car["car_no"])
129
- case "high":
130
- return sorted(cars, key=lambda car: car["car_no"], reverse=True)
131
- case _:
132
- raise ValueError(f"unsupported car priority: {priority}")
133
-
134
-
135
- def seat_preference_key(seat: SrtSeat, priority: str = "forward-window") -> tuple[int, int, int, str]:
136
- row, column = parse_seat_label(seat["seat"])
137
- forward_rank = 0 if seat["direction"] == "정방향" else 1
138
- window_rank = 0 if seat["position"] in {"창측", "1인석"} else 1
139
- row_rank = 999 if row is None else row
140
- match priority:
141
- case "forward-window":
142
- return (forward_rank, window_rank, row_rank, column)
143
- case "window-forward":
144
- return (window_rank, forward_rank, row_rank, column)
145
- case "row-low":
146
- return (row_rank, forward_rank, window_rank, column)
147
- case _:
148
- raise ValueError(f"unsupported seat priority: {priority}")
149
-
150
-
151
- def sort_seats_for_booking(seats: list[SrtSeat], priority: str = "forward-window") -> list[SrtSeat]:
152
- return sorted(seats, key=lambda seat: seat_preference_key(seat, priority))
153
-
154
-
155
- sort_cars = sort_cars_for_booking
156
- sort_seats = sort_seats_for_booking
@@ -1,109 +0,0 @@
1
- # Used Car Price Search
2
-
3
- ## What this skill does
4
-
5
- 한국의 대표 렌터카 업체를 먼저 비교하고, 현재는 **가장 기술적으로 구현이 쉬운 공급자**로 확인된 `SK렌터카 다이렉트 타고BUY`를 사용해 중고차 가격을 조회한다.
6
-
7
- - 한국의 주요 렌터카 업체로 `SK렌터카`, `롯데렌탈(롯데오토옥션)`, `레드캡렌터카`를 먼저 확인한다.
8
- - 각 업체의 공개 표면에서 **직접 API 제공 여부**, 웹 크롤링 난이도, 기존 **MCP / Skill** 존재 여부를 먼저 점검한다.
9
- - 이 저장소와 현재 세션에는 중고차 가격 조회용 전용 **MCP** 나 **Skill** 이 없으므로 새 스킬이 직접 조회를 담당한다.
10
- - 최종 선택 공급자는 `https://www.skdirect.co.kr/tb` 이다.
11
- - 이 페이지는 로그인 없이 열리고, HTML 안의 `__NEXT_DATA__` 에 현재 inventory snapshot 이 들어 있어 반복 조회가 쉽다.
12
- - 결과는 **월 렌트료**와 **인수가**를 함께 보여 준다.
13
-
14
- ## Provider survey
15
-
16
- | 업체 | 점검 결과 | v1 채택 여부 |
17
- | --- | --- | --- |
18
- | SK렌터카 다이렉트 `타고BUY` | `https://www.skdirect.co.kr/tb` 공개 HTML 에 `__NEXT_DATA__` inventory snapshot 포함. 로그인/세션 없이 반복 조회 가능. 별도 공개 API 문서는 못 찾았지만 SSR 데이터 추출이 가장 단순함. | 채택 |
19
- | 롯데렌탈 / 롯데오토옥션 | `https://www.lotteautoauction.net/hp/pub/cmm/viewMain.do` 공개 진입점은 열리지만, 일반 매물 검색은 legacy `.do` 흐름 중심이고 공개 목록 계약이 불명확했다. 추정 목록 URL은 404/에러 페이지가 섞여 v1 공급자로는 불안정했다. | 미채택 |
20
- | 레드캡렌터카 | 공식 진입점이 `https://biz.redcap.co.kr/rent/` business portal 로 이어졌고, 공개 중고차 inventory 검색 표면이나 직접 API를 확인하지 못했다. | 미채택 |
21
-
22
- ## When to use
23
-
24
- - "아반떼 중고차 가격 봐줘"
25
- - "SK렌터카 타고BUY에 K3 얼마야?"
26
- - "캐스퍼 인수가/월 렌트료 같이 알려줘"
27
- - "중고차 시세를 렌터카 업체 기준으로 보고 싶어"
28
-
29
- ## When not to use
30
-
31
- - 실제 구매/계약/상담 신청까지 자동화해야 하는 경우
32
- - 특정 VIN/성능기록부/사고이력 원문까지 강제해야 하는 경우
33
- - 여러 업체 통합 최저가 비교가 필요한 경우
34
-
35
- ## Prerequisites
36
-
37
- - 인터넷 연결
38
- - `node` 18+
39
- - `used-car-price-search` package 또는 동일 로직
40
-
41
- ## Required inputs
42
-
43
- ### 1. Ask the car model/keyword first if it is missing
44
-
45
- 차종 키워드가 없으면 먼저 물어본다.
46
-
47
- - 권장 질문: `어떤 차종을 찾을까요? 예: 아반떼, K3, 캐스퍼`
48
- - 너무 넓으면: `제조사나 차종을 조금 더 구체적으로 알려주세요. 예: 현대 아반떼, 기아 K3`
49
-
50
- ## Official surface used in v1
51
-
52
- - SK direct used-car inventory page: `https://www.skdirect.co.kr/tb`
53
-
54
- ## Workflow
55
-
56
- 1. 차종 키워드가 없으면 먼저 질문한다.
57
- 2. `SK렌터카`, `롯데렌탈`, `레드캡렌터카` 비교 결과를 짧게 기억하고, 현재 공급자는 `SK렌터카 다이렉트 타고BUY` 임을 유지한다.
58
- 3. `https://www.skdirect.co.kr/tb` HTML 을 가져온다.
59
- 4. HTML 의 `__NEXT_DATA__` JSON 에서 `carListProd` inventory snapshot 을 읽는다.
60
- 5. 차종 키워드로 inventory 를 필터링한다.
61
- 6. 상위 결과에서 `인수가`, `월 렌트료`, `연식`, `주행거리`, `연료`, `변속기`를 정리한다.
62
- 7. 같은 차종이라도 재고가 수시로 바뀔 수 있으므로 snapshot 기준 응답임을 짧게 알린다.
63
-
64
- ## Node.js example
65
-
66
- ```js
67
- const { lookupUsedCarPrices } = require("used-car-price-search")
68
-
69
- async function main() {
70
- const result = await lookupUsedCarPrices("아반떼", { limit: 5 })
71
-
72
- console.log({
73
- provider: result.provider,
74
- matchedCount: result.matchedCount,
75
- summary: result.summary,
76
- items: result.items
77
- })
78
- }
79
-
80
- main().catch((error) => {
81
- console.error(error)
82
- process.exitCode = 1
83
- })
84
- ```
85
-
86
- ## Respond conservatively
87
-
88
- 응답은 아래 순서로 짧게 정리한다.
89
-
90
- - 공급자: `SK렌터카 다이렉트 타고BUY`
91
- - 차종 키워드
92
- - 매칭된 차량 수
93
- - 인수가 범위
94
- - 월 렌트료 범위
95
- - 대표 차량 2~5개
96
- - `공개 inventory snapshot 기준이라 실시간 재고/가격은 바뀔 수 있다`는 안내
97
-
98
- ## Done when
99
-
100
- - 주요 렌터카 업체 비교와 공급자 선택 이유를 설명했다.
101
- - 차종 키워드 기준으로 결과를 최소 1건 이상 또는 보수적 빈 결과로 반환했다.
102
- - 결과에 `인수가` 와 `월 렌트료` 를 함께 담았다.
103
- - 라이브 검증에서 **최소 10회 이상** 반복 조회가 가능함을 확인했다.
104
-
105
- ## Failure modes
106
-
107
- - 공개 inventory snapshot 은 페이지 갱신 타이밍에 따라 달라질 수 있다.
108
- - 별도 공개 API 문서는 찾지 못했으므로 v1 은 HTML 내 `__NEXT_DATA__` 의 안정성에 의존한다.
109
- - 특정 차종 키워드가 너무 넓으면 유사 모델이 함께 섞일 수 있다.
@@ -1,9 +0,0 @@
1
- # 상표 사용 법적 고지 — `used-car-price-search`
2
-
3
- 이 스킬에서 `SK렌터카` 명칭은 중고차 재고·가격·인수가 조회의 **대상 판매·렌터카 서비스**를 식별하기 위해 사용한다. k-skill의 출처를 SK렌터카 운영사로 표시하려는 사용이 아니다.
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조)와 대법원 [2011다18802](https://www.law.go.kr/LSW/precInfoP.do?precSeq=167457), [2019후10418](https://law.go.kr/LSW/precInfoP.do?mode=0&precSeq=230725) 판결에 따라 실제 거래계에서 출처표시로 기능하는지는 표시 태양과 사용 경위 등을 종합해 판단해야 한다.
6
-
7
- 따라서 이 명칭은 필요한 범위의 평문으로만 사용하고, 별도 근거 없이 로고, 공식·제휴·후원·인증·파트너 표현을 사용하지 않는다. 각 상표의 권리는 해당 권리자에게 있다. 이 문서는 차량정보의 저작권·데이터베이스권, 매매·렌탈 계약, 약관·계약, 소비자보호 또는 접근 방식의 적법성을 판단하지 않는다.
8
-
9
- 전체 검토: [제3자 상표의 기능 설명·호환 대상 표시 검토](https://github.com/NomaDamas/k-skill/blob/dev/docs/legal/trademark-use-review.md)
@@ -1,8 +0,0 @@
1
- {
2
- "name": "used-car-price-search",
3
- "description": "주요 한국 렌터카 업체를 비교한 뒤 SK렌터카 다이렉트 타고BUY inventory snapshot 으로 중고차 가격/인수가를 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
4
- "profiles": [
5
- "action:commerce"
6
- ],
7
- "frontmatter": "name: used-car-price-search\ndescription: 주요 한국 렌터카 업체를 비교한 뒤 SK렌터카 다이렉트 타고BUY inventory snapshot 으로 중고차 가격/인수가를 조회한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: automotive\n locale: ko-KR\n phase: v1"
8
- }