@nomadamas/k-skill 0.1.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 (350) hide show
  1. package/bin/k-skill.js +60 -0
  2. package/package.json +37 -0
  3. package/skills/assembly-bill-vote-search/instruction.md +111 -0
  4. package/skills/assembly-bill-vote-search/skill.json +10 -0
  5. package/skills/biz-health-check/instruction.md +69 -0
  6. package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
  7. package/skills/biz-health-check/skill.json +10 -0
  8. package/skills/bok-ecos-stats/instruction.md +95 -0
  9. package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
  10. package/skills/bok-ecos-stats/skill.json +10 -0
  11. package/skills/building-register-search/instruction.md +69 -0
  12. package/skills/building-register-search/scripts/building_register.py +309 -0
  13. package/skills/building-register-search/scripts/building_register_xml.py +32 -0
  14. package/skills/building-register-search/skill.json +9 -0
  15. package/skills/bunjang-search/instruction.md +154 -0
  16. package/skills/bunjang-search/skill.json +10 -0
  17. package/skills/catchtable-sniper/instruction.md +270 -0
  18. package/skills/catchtable-sniper/skill.json +9 -0
  19. package/skills/cheap-gas-nearby/instruction.md +107 -0
  20. package/skills/cheap-gas-nearby/skill.json +10 -0
  21. package/skills/corporate-registration-consulting/instruction.md +117 -0
  22. package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
  23. package/skills/corporate-registration-consulting/skill.json +8 -0
  24. package/skills/coupang-product-search/instruction.md +219 -0
  25. package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
  26. package/skills/coupang-product-search/skill.json +10 -0
  27. package/skills/court-auction-notice-search/instruction.md +201 -0
  28. package/skills/court-auction-notice-search/skill.json +9 -0
  29. package/skills/court-payment-order-assistant/instruction.md +120 -0
  30. package/skills/court-payment-order-assistant/skill.json +10 -0
  31. package/skills/d2b-notice-search/instruction.md +117 -0
  32. package/skills/d2b-notice-search/skill.json +10 -0
  33. package/skills/daangn-cars-search/instruction.md +91 -0
  34. package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
  35. package/skills/daangn-cars-search/skill.json +9 -0
  36. package/skills/daangn-jobs-search/instruction.md +90 -0
  37. package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
  38. package/skills/daangn-jobs-search/skill.json +9 -0
  39. package/skills/daangn-realty-search/instruction.md +114 -0
  40. package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
  41. package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
  42. package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
  43. package/skills/daangn-realty-search/skill.json +8 -0
  44. package/skills/daangn-used-goods-search/instruction.md +90 -0
  45. package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
  46. package/skills/daangn-used-goods-search/skill.json +9 -0
  47. package/skills/daishin-report-search/instruction.md +138 -0
  48. package/skills/daishin-report-search/skill.json +10 -0
  49. package/skills/daiso-product-search/instruction.md +172 -0
  50. package/skills/daiso-product-search/skill.json +8 -0
  51. package/skills/danawa-price-search/instruction.md +183 -0
  52. package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
  53. package/skills/danawa-price-search/skill.json +8 -0
  54. package/skills/delivery-tracking/instruction.md +358 -0
  55. package/skills/delivery-tracking/skill.json +8 -0
  56. package/skills/donation-place-search/instruction.md +129 -0
  57. package/skills/donation-place-search/skill.json +9 -0
  58. package/skills/emergency-room-beds/instruction.md +82 -0
  59. package/skills/emergency-room-beds/skill.json +8 -0
  60. package/skills/ev-charger-nearby/instruction.md +80 -0
  61. package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
  62. package/skills/ev-charger-nearby/skill.json +10 -0
  63. package/skills/ev-subsidy-status/instruction.md +165 -0
  64. package/skills/ev-subsidy-status/skill.json +10 -0
  65. package/skills/express-bus-booking/instruction.md +207 -0
  66. package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
  67. package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
  68. package/skills/express-bus-booking/skill.json +8 -0
  69. package/skills/fine-dust-location/instruction.md +89 -0
  70. package/skills/fine-dust-location/skill.json +9 -0
  71. package/skills/flight-ticket-search/instruction.md +237 -0
  72. package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
  73. package/skills/flight-ticket-search/skill.json +9 -0
  74. package/skills/foresttrip-vacancy/instruction.md +167 -0
  75. package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
  76. package/skills/foresttrip-vacancy/skill.json +10 -0
  77. package/skills/fsc-corporate-info/instruction.md +57 -0
  78. package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
  79. package/skills/fsc-corporate-info/skill.json +10 -0
  80. package/skills/g2b-order-plan-search/instruction.md +131 -0
  81. package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
  82. package/skills/g2b-order-plan-search/skill.json +10 -0
  83. package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
  84. package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
  85. package/skills/g2b-sanctioned-supplier/skill.json +10 -0
  86. package/skills/gangnamunni-clinic-search/instruction.md +113 -0
  87. package/skills/gangnamunni-clinic-search/skill.json +9 -0
  88. package/skills/geeknews-search/instruction.md +69 -0
  89. package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
  90. package/skills/geeknews-search/skill.json +8 -0
  91. package/skills/gongsijiga-search/instruction.md +128 -0
  92. package/skills/gongsijiga-search/skill.json +9 -0
  93. package/skills/gov-overseas-trip-report/instruction.md +488 -0
  94. package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
  95. package/skills/gov-overseas-trip-report/skill.json +9 -0
  96. package/skills/han-river-water-level/instruction.md +83 -0
  97. package/skills/han-river-water-level/skill.json +10 -0
  98. package/skills/highway-traffic-status/instruction.md +88 -0
  99. package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
  100. package/skills/highway-traffic-status/skill.json +11 -0
  101. package/skills/hipass-receipt/instruction.md +97 -0
  102. package/skills/hipass-receipt/skill.json +10 -0
  103. package/skills/hola-poke-yeoksam/instruction.md +247 -0
  104. package/skills/hola-poke-yeoksam/skill.json +8 -0
  105. package/skills/household-waste-info/instruction.md +117 -0
  106. package/skills/household-waste-info/skill.json +10 -0
  107. package/skills/housing-official-price/instruction.md +177 -0
  108. package/skills/housing-official-price/skill.json +10 -0
  109. package/skills/hwp/instruction.md +206 -0
  110. package/skills/hwp/skill.json +8 -0
  111. package/skills/intercity-bus-booking/instruction.md +189 -0
  112. package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
  113. package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
  114. package/skills/intercity-bus-booking/skill.json +8 -0
  115. package/skills/iros-registry-automation/instruction.md +229 -0
  116. package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
  117. package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
  118. package/skills/iros-registry-automation/skill.json +9 -0
  119. package/skills/job-posting-match/instruction.md +130 -0
  120. package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
  121. package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
  122. package/skills/job-posting-match/skill.json +8 -0
  123. package/skills/jobkorea-talent-search/instruction.md +118 -0
  124. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
  125. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
  126. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
  127. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
  128. package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
  129. package/skills/jobkorea-talent-search/skill.json +9 -0
  130. package/skills/joseon-sillok-search/instruction.md +76 -0
  131. package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
  132. package/skills/joseon-sillok-search/skill.json +8 -0
  133. package/skills/k-dart/instruction.md +406 -0
  134. package/skills/k-dart/skill.json +8 -0
  135. package/skills/k-schoollunch-menu/instruction.md +109 -0
  136. package/skills/k-schoollunch-menu/skill.json +9 -0
  137. package/skills/k-skill-cleaner/instruction.md +80 -0
  138. package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
  139. package/skills/k-skill-cleaner/skill.json +8 -0
  140. package/skills/k-skill-setup/instruction.md +253 -0
  141. package/skills/k-skill-setup/skill.json +11 -0
  142. package/skills/kakao-bar-nearby/instruction.md +76 -0
  143. package/skills/kakao-bar-nearby/skill.json +8 -0
  144. package/skills/kakao-map/instruction.md +176 -0
  145. package/skills/kakao-map/skill.json +10 -0
  146. package/skills/kakaotalk-mac/instruction.md +189 -0
  147. package/skills/kakaotalk-mac/skill.json +8 -0
  148. package/skills/kbl-results/instruction.md +89 -0
  149. package/skills/kbl-results/skill.json +9 -0
  150. package/skills/kbo-results/instruction.md +82 -0
  151. package/skills/kbo-results/skill.json +8 -0
  152. package/skills/keris-academic-search/instruction.md +81 -0
  153. package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
  154. package/skills/keris-academic-search/skill.json +10 -0
  155. package/skills/kleague-results/instruction.md +92 -0
  156. package/skills/kleague-results/skill.json +8 -0
  157. package/skills/kopis-performance-search/instruction.md +109 -0
  158. package/skills/kopis-performance-search/skill.json +10 -0
  159. package/skills/korea-weather/instruction.md +93 -0
  160. package/skills/korea-weather/skill.json +10 -0
  161. package/skills/korean-character-count/instruction.md +87 -0
  162. package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
  163. package/skills/korean-character-count/skill.json +8 -0
  164. package/skills/korean-cinema-search/instruction.md +177 -0
  165. package/skills/korean-cinema-search/skill.json +8 -0
  166. package/skills/korean-heritage-search/instruction.md +106 -0
  167. package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
  168. package/skills/korean-heritage-search/skill.json +8 -0
  169. package/skills/korean-holiday-calendar/instruction.md +97 -0
  170. package/skills/korean-holiday-calendar/skill.json +9 -0
  171. package/skills/korean-humanizer/instruction.md +389 -0
  172. package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
  173. package/skills/korean-humanizer/skill.json +8 -0
  174. package/skills/korean-jangbu-for/instruction.md +133 -0
  175. package/skills/korean-jangbu-for/scripts/install.sh +237 -0
  176. package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
  177. package/skills/korean-jangbu-for/skill.json +8 -0
  178. package/skills/korean-law-search/instruction.md +126 -0
  179. package/skills/korean-law-search/skill.json +10 -0
  180. package/skills/korean-marathon-schedule/instruction.md +111 -0
  181. package/skills/korean-marathon-schedule/skill.json +9 -0
  182. package/skills/korean-middle-korean/instruction.md +79 -0
  183. package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
  184. package/skills/korean-middle-korean/skill.json +8 -0
  185. package/skills/korean-patent-search/instruction.md +79 -0
  186. package/skills/korean-patent-search/scripts/patent_search.py +409 -0
  187. package/skills/korean-patent-search/skill.json +9 -0
  188. package/skills/korean-privacy-terms/instruction.md +128 -0
  189. package/skills/korean-privacy-terms/scripts/install.sh +108 -0
  190. package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
  191. package/skills/korean-privacy-terms/skill.json +8 -0
  192. package/skills/korean-scholarship-search/instruction.md +317 -0
  193. package/skills/korean-scholarship-search/references/report-format.md +40 -0
  194. package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
  195. package/skills/korean-scholarship-search/references/search-clues.md +58 -0
  196. package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
  197. package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
  198. package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
  199. package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
  200. package/skills/korean-scholarship-search/skill.json +8 -0
  201. package/skills/korean-slang-writing/instruction.md +181 -0
  202. package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
  203. package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
  204. package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
  205. package/skills/korean-slang-writing/skill.json +8 -0
  206. package/skills/korean-spell-check/instruction.md +105 -0
  207. package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
  208. package/skills/korean-spell-check/skill.json +9 -0
  209. package/skills/korean-stock-search/instruction.md +194 -0
  210. package/skills/korean-stock-search/skill.json +10 -0
  211. package/skills/korean-transit-route/instruction.md +113 -0
  212. package/skills/korean-transit-route/skill.json +10 -0
  213. package/skills/kosis-stats/instruction.md +232 -0
  214. package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
  215. package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
  216. package/skills/kosis-stats/skill.json +9 -0
  217. package/skills/kr-whois-lookup/instruction.md +107 -0
  218. package/skills/kr-whois-lookup/skill.json +10 -0
  219. package/skills/kstartup-search/instruction.md +186 -0
  220. package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
  221. package/skills/kstartup-search/skill.json +11 -0
  222. package/skills/ktx-booking/instruction.md +244 -0
  223. package/skills/ktx-booking/skill.json +10 -0
  224. package/skills/lck-analytics/instruction.md +192 -0
  225. package/skills/lck-analytics/scripts/_lib.js +103 -0
  226. package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
  227. package/skills/lck-analytics/scripts/build-match-report.js +44 -0
  228. package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
  229. package/skills/lck-analytics/skill.json +9 -0
  230. package/skills/lh-notice-search/instruction.md +206 -0
  231. package/skills/lh-notice-search/skill.json +10 -0
  232. package/skills/library-book-search/instruction.md +139 -0
  233. package/skills/library-book-search/skill.json +9 -0
  234. package/skills/local-election-candidate-search/instruction.md +77 -0
  235. package/skills/local-election-candidate-search/skill.json +8 -0
  236. package/skills/localdata-business-status/instruction.md +64 -0
  237. package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
  238. package/skills/localdata-business-status/skill.json +8 -0
  239. package/skills/lotto-results/instruction.md +80 -0
  240. package/skills/lotto-results/skill.json +8 -0
  241. package/skills/lovebug-report/instruction.md +185 -0
  242. package/skills/lovebug-report/skill.json +9 -0
  243. package/skills/market-kurly-search/instruction.md +125 -0
  244. package/skills/market-kurly-search/skill.json +8 -0
  245. package/skills/mfds-drug-safety/instruction.md +86 -0
  246. package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
  247. package/skills/mfds-drug-safety/skill.json +10 -0
  248. package/skills/mfds-food-safety/instruction.md +126 -0
  249. package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
  250. package/skills/mfds-food-safety/skill.json +10 -0
  251. package/skills/myrealtrip-search/instruction.md +239 -0
  252. package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
  253. package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
  254. package/skills/myrealtrip-search/skill.json +9 -0
  255. package/skills/naming-house/instruction.md +146 -0
  256. package/skills/naming-house/skill.json +8 -0
  257. package/skills/national-pension-workplace/instruction.md +64 -0
  258. package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
  259. package/skills/national-pension-workplace/skill.json +10 -0
  260. package/skills/naver-ad-performance/instruction.md +108 -0
  261. package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
  262. package/skills/naver-ad-performance/skill.json +9 -0
  263. package/skills/naver-blog-research/instruction.md +128 -0
  264. package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
  265. package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
  266. package/skills/naver-blog-research/scripts/naver_read.py +256 -0
  267. package/skills/naver-blog-research/scripts/naver_search.py +192 -0
  268. package/skills/naver-blog-research/skill.json +8 -0
  269. package/skills/naver-news-search/instruction.md +103 -0
  270. package/skills/naver-news-search/skill.json +10 -0
  271. package/skills/naver-shopping-search/instruction.md +94 -0
  272. package/skills/naver-shopping-search/skill.json +11 -0
  273. package/skills/nhis-care-checkup-search/instruction.md +116 -0
  274. package/skills/nhis-care-checkup-search/skill.json +10 -0
  275. package/skills/nts-business-registration/instruction.md +115 -0
  276. package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
  277. package/skills/nts-business-registration/skill.json +10 -0
  278. package/skills/nts-tax-delinquency/instruction.md +54 -0
  279. package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
  280. package/skills/nts-tax-delinquency/skill.json +8 -0
  281. package/skills/ohou-today-deal/instruction.md +182 -0
  282. package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
  283. package/skills/ohou-today-deal/skill.json +9 -0
  284. package/skills/olive-young-search/instruction.md +154 -0
  285. package/skills/olive-young-search/skill.json +8 -0
  286. package/skills/parking-lot-search/instruction.md +96 -0
  287. package/skills/parking-lot-search/skill.json +9 -0
  288. package/skills/popbill/instruction.md +146 -0
  289. package/skills/popbill/scripts/popbill_cli.py +284 -0
  290. package/skills/popbill/scripts/popbill_registry.py +54 -0
  291. package/skills/popbill/scripts/popbill_safety.py +37 -0
  292. package/skills/popbill/scripts/popbill_templates.py +71 -0
  293. package/skills/popbill/skill.json +9 -0
  294. package/skills/public-restroom-nearby/instruction.md +89 -0
  295. package/skills/public-restroom-nearby/skill.json +9 -0
  296. package/skills/real-estate-search/instruction.md +172 -0
  297. package/skills/real-estate-search/skill.json +10 -0
  298. package/skills/rhwp-advanced/instruction.md +145 -0
  299. package/skills/rhwp-advanced/skill.json +8 -0
  300. package/skills/rhwp-edit/instruction.md +153 -0
  301. package/skills/rhwp-edit/skill.json +8 -0
  302. package/skills/s2b-notice-search/instruction.md +66 -0
  303. package/skills/s2b-notice-search/skill.json +9 -0
  304. package/skills/saju-fortune/instruction.md +177 -0
  305. package/skills/saju-fortune/skill.json +8 -0
  306. package/skills/saramin-talent-search/instruction.md +119 -0
  307. package/skills/saramin-talent-search/skill.json +9 -0
  308. package/skills/seoul-bike/instruction.md +83 -0
  309. package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
  310. package/skills/seoul-bike/skill.json +10 -0
  311. package/skills/seoul-density/instruction.md +109 -0
  312. package/skills/seoul-density/scripts/seoul_density.py +271 -0
  313. package/skills/seoul-density/skill.json +10 -0
  314. package/skills/seoul-subway-arrival/instruction.md +85 -0
  315. package/skills/seoul-subway-arrival/skill.json +9 -0
  316. package/skills/sh-notice-search/instruction.md +150 -0
  317. package/skills/sh-notice-search/skill.json +9 -0
  318. package/skills/srt-booking/instruction.md +181 -0
  319. package/skills/srt-booking/scripts/srt_booking.py +272 -0
  320. package/skills/srt-booking/scripts/srt_seats.py +156 -0
  321. package/skills/srt-booking/skill.json +20 -0
  322. package/skills/subway-lost-property/instruction.md +93 -0
  323. package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
  324. package/skills/subway-lost-property/skill.json +8 -0
  325. package/skills/ticket-availability/instruction.md +175 -0
  326. package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
  327. package/skills/ticket-availability/skill.json +9 -0
  328. package/skills/toss-securities/instruction.md +116 -0
  329. package/skills/toss-securities/skill.json +9 -0
  330. package/skills/used-car-price-search/instruction.md +109 -0
  331. package/skills/used-car-price-search/skill.json +8 -0
  332. package/skills/yebigun-training/instruction.md +177 -0
  333. package/skills/yebigun-training/skill.json +9 -0
  334. package/skills/zipcode-search/instruction.md +159 -0
  335. package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
  336. package/skills/zipcode-search/skill.json +8 -0
  337. package/src/assemble.js +134 -0
  338. package/src/detect.js +18 -0
  339. package/templates/action-account.md +4 -0
  340. package/templates/action-booking.md +5 -0
  341. package/templates/action-commerce.md +5 -0
  342. package/templates/action-communication.md +5 -0
  343. package/templates/action-submission.md +5 -0
  344. package/templates/browser.md +4 -0
  345. package/templates/core.md +5 -0
  346. package/templates/hard-boundary.md +3 -0
  347. package/templates/local.md +2 -0
  348. package/templates/lookup.md +2 -0
  349. package/templates/proxy.md +2 -0
  350. package/templates/vault.md +4 -0
@@ -0,0 +1,552 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import html
5
+ import json
6
+ import math
7
+ import re
8
+ import ssl
9
+ import sys
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+ from dataclasses import asdict, dataclass
14
+ from http.cookiejar import CookieJar
15
+ from typing import Any, Iterable
16
+
17
+ try:
18
+ import requests
19
+ except ImportError: # pragma: no cover - optional runtime dependency
20
+ requests = None
21
+
22
+
23
+ BASE_URL = "https://sillok.history.go.kr"
24
+ SEARCH_URL = f"{BASE_URL}/search/searchResultList.do"
25
+ DETAIL_URL_TEMPLATE = f"{BASE_URL}/id/{{article_id}}"
26
+ DEFAULT_TIMEOUT = 30
27
+ DEFAULT_LIMIT = 5
28
+ MAX_PAGES = 20
29
+
30
+ DEFAULT_HEADERS = {
31
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
32
+ "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
33
+ "Content-Type": "application/x-www-form-urlencoded",
34
+ "Origin": BASE_URL,
35
+ "Referer": f"{BASE_URL}/main/main.do",
36
+ "User-Agent": (
37
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
38
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
39
+ ),
40
+ }
41
+
42
+ COMMENT_PATTERN = re.compile(r"<!--.*?-->", re.S)
43
+ BR_PATTERN = re.compile(r"<br\s*/?>", re.I)
44
+ TAG_PATTERN = re.compile(r"<[^>]+>")
45
+ WHITESPACE_PATTERN = re.compile(r"\s+")
46
+ DIGITS_PATTERN = re.compile(r"\d+")
47
+ DETAIL_FOOTER_PATTERN = re.compile(
48
+ r"\s*(?:[【〖](?:태백산사고본|국편영인본|분류)[】〗]|ⓒ\s*세종대왕기념사업회).*",
49
+ re.S,
50
+ )
51
+
52
+ CATEGORY_PATTERN = re.compile(
53
+ r"<a\s+href=\"javascript:searchCategory\('([^']*)'\);\"\s+class=\"cate-item[^\"]*\">(.*?)</a>",
54
+ re.S,
55
+ )
56
+ RESULT_PATTERN = re.compile(
57
+ r"<div\s+class=\"result-box\">.*?"
58
+ r"<a\s+href=\"javascript:goView\('([^']+)',\s*\d+\);\"\s+class=\"subject\">(.*?)</a>\s*"
59
+ r"<p\s+class=\"text\">(.*?)</p>",
60
+ re.S,
61
+ )
62
+ TITLE_HEAD_PATTERN = re.compile(
63
+ r"<div\s+class=\"title\">\s*<p\s+class=\"date\">(.*?)</p>\s*<h3>(.*?)</h3>",
64
+ re.S,
65
+ )
66
+ LEFT_VIEW_PATTERN = re.compile(
67
+ r"<div\s+class=\"view-item\s+left\">.*?<div\s+class=\"view-text\">(.*?)</div>",
68
+ re.S,
69
+ )
70
+ RIGHT_VIEW_PATTERN = re.compile(
71
+ r"<div\s+class=\"view-item\s+right\">.*?<div\s+class=\"view-text\">(.*?)</div>",
72
+ re.S,
73
+ )
74
+ CLASSIFICATION_PATTERN = re.compile(r"<li\s+class=\"view_font02\">\s*[【〖]분류[】〗]\s*(.*?)</li>", re.S)
75
+ TOTAL_COUNT_PATTERN = re.compile(r"검색결과\s*<strong>(\d+)</strong>개")
76
+ HIDDEN_VALUE_TEMPLATE = '<input[^>]+id="{field}"[^>]+value="([^"]*)"'
77
+
78
+ KING_ACCESSION_YEARS = {
79
+ "태조": 1392,
80
+ "정종": 1399,
81
+ "태종": 1401,
82
+ "세종": 1418,
83
+ "문종": 1450,
84
+ "단종": 1452,
85
+ "세조": 1455,
86
+ "예종": 1468,
87
+ "성종": 1469,
88
+ "연산군": 1494,
89
+ "중종": 1506,
90
+ "인종": 1545,
91
+ "명종": 1545,
92
+ "선조": 1567,
93
+ "선조수정": 1567,
94
+ "광해군중초본": 1608,
95
+ "광해군정초본": 1608,
96
+ "인조": 1623,
97
+ "효종": 1649,
98
+ "현종": 1659,
99
+ "현종개수": 1659,
100
+ "숙종": 1674,
101
+ "숙종보궐정오": 1674,
102
+ "경종": 1720,
103
+ "경종수정": 1720,
104
+ "영조": 1724,
105
+ "정조": 1776,
106
+ "순조": 1800,
107
+ "헌종": 1834,
108
+ "철종": 1849,
109
+ "고종": 1863,
110
+ "순종": 1907,
111
+ "순종부록": 1910,
112
+ }
113
+
114
+ KING_ALIASES = {
115
+ "태조실록": "태조",
116
+ "정종실록": "정종",
117
+ "태종실록": "태종",
118
+ "세종실록": "세종",
119
+ "문종실록": "문종",
120
+ "단종실록": "단종",
121
+ "세조실록": "세조",
122
+ "예종실록": "예종",
123
+ "성종실록": "성종",
124
+ "연산군일기": "연산군",
125
+ "중종실록": "중종",
126
+ "인종실록": "인종",
127
+ "명종실록": "명종",
128
+ "선조실록": "선조",
129
+ "선조수정실록": "선조수정",
130
+ "광해군중초본": "광해군중초본",
131
+ "광해군정초본": "광해군정초본",
132
+ "인조실록": "인조",
133
+ "효종실록": "효종",
134
+ "현종실록": "현종",
135
+ "현종개수실록": "현종개수",
136
+ "숙종실록": "숙종",
137
+ "숙종보궐정오": "숙종보궐정오",
138
+ "경종실록": "경종",
139
+ "경종수정실록": "경종수정",
140
+ "영조실록": "영조",
141
+ "정조실록": "정조",
142
+ "순조실록": "순조",
143
+ "헌종실록": "헌종",
144
+ "철종실록": "철종",
145
+ "고종실록": "고종",
146
+ "순종실록": "순종",
147
+ "순종실록부록": "순종부록",
148
+ "순종부록": "순종부록",
149
+ }
150
+
151
+ for canonical in KING_ACCESSION_YEARS:
152
+ KING_ALIASES.setdefault(canonical, canonical)
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class SearchCategory:
157
+ label: str
158
+ count: int
159
+ token: str
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class ResultTitleMetadata:
164
+ king: str | None
165
+ regnal_year: int | None
166
+ gregorian_year: int | None
167
+ article_title: str
168
+
169
+
170
+ @dataclass(frozen=True)
171
+ class SearchResult:
172
+ article_id: str
173
+ url: str
174
+ title: str
175
+ article_title: str
176
+ summary: str
177
+ king: str | None
178
+ regnal_year: int | None
179
+ gregorian_year: int | None
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class SearchReport:
184
+ query: str
185
+ search_type: str
186
+ total_results: int
187
+ type_count: int
188
+ categories: list[SearchCategory]
189
+ items: list[SearchResult]
190
+
191
+
192
+ @dataclass(frozen=True)
193
+ class ArticleDetail:
194
+ article_id: str
195
+ url: str
196
+ header: str
197
+ title: str
198
+ translated_text: str
199
+ original_text: str
200
+ classification: str | None
201
+
202
+
203
+ def clean_text(value: str | None) -> str:
204
+ text = COMMENT_PATTERN.sub(" ", value or "")
205
+ text = BR_PATTERN.sub("\n", text)
206
+ text = TAG_PATTERN.sub(" ", text)
207
+ text = html.unescape(text)
208
+ return WHITESPACE_PATTERN.sub(" ", text).strip()
209
+
210
+
211
+ def clean_article_text(value: str | None) -> str:
212
+ text = COMMENT_PATTERN.sub(" ", value or "")
213
+ text = BR_PATTERN.sub("\n", text)
214
+ text = TAG_PATTERN.sub(" ", text)
215
+ text = html.unescape(text)
216
+ lines = [WHITESPACE_PATTERN.sub(" ", line).strip() for line in text.splitlines()]
217
+ text = "\n".join(line for line in lines if line)
218
+ text = DETAIL_FOOTER_PATTERN.sub("", text).strip()
219
+ return WHITESPACE_PATTERN.sub(" ", text).strip()
220
+
221
+
222
+ def positive_int(raw_value: str) -> int:
223
+ value = int(raw_value)
224
+ if value <= 0:
225
+ raise argparse.ArgumentTypeError("must be a positive integer")
226
+ return value
227
+
228
+
229
+ def normalize_king_name(value: str | None) -> str | None:
230
+ if value is None:
231
+ return None
232
+ cleaned = clean_text(value).replace(" ", "")
233
+ if not cleaned:
234
+ return None
235
+ return KING_ALIASES.get(cleaned, cleaned)
236
+
237
+
238
+ def build_opener() -> urllib.request.OpenerDirector:
239
+ cookie_jar = CookieJar()
240
+ context = ssl.create_default_context()
241
+ return urllib.request.build_opener(
242
+ urllib.request.HTTPCookieProcessor(cookie_jar),
243
+ urllib.request.HTTPSHandler(context=context),
244
+ )
245
+
246
+
247
+ def build_http_client() -> Any:
248
+ return build_opener()
249
+
250
+
251
+ def should_fallback_to_opener(error: Exception) -> bool:
252
+ if requests is None:
253
+ return False
254
+
255
+ exceptions = getattr(requests, "exceptions", None)
256
+ http_error = getattr(exceptions, "HTTPError", None)
257
+ if isinstance(http_error, type) and isinstance(error, http_error):
258
+ return False
259
+
260
+ request_exception = getattr(exceptions, "RequestException", None)
261
+ if isinstance(request_exception, type) and isinstance(error, request_exception):
262
+ return True
263
+
264
+ return isinstance(error, OSError)
265
+
266
+
267
+ def fetch_text(
268
+ opener: Any,
269
+ url: str,
270
+ *,
271
+ data: dict[str, str] | None = None,
272
+ timeout: int = DEFAULT_TIMEOUT,
273
+ referer: str | None = None,
274
+ ) -> str:
275
+ headers = dict(DEFAULT_HEADERS)
276
+ if referer is not None:
277
+ headers["Referer"] = referer
278
+
279
+ if requests is not None:
280
+ try:
281
+ if data is not None:
282
+ response = requests.post(url, data=data, timeout=timeout, headers=headers)
283
+ else:
284
+ response = requests.get(url, timeout=timeout, headers=headers)
285
+ response.raise_for_status()
286
+ return response.text
287
+ except Exception as error: # noqa: BLE001
288
+ if opener is None or not should_fallback_to_opener(error):
289
+ raise RuntimeError(f"Sillok request failed for {url}: {error}") from error
290
+
291
+ body = urllib.parse.urlencode(data).encode("utf-8") if data is not None else None
292
+ request = urllib.request.Request(url, data=body, headers=headers, method="POST" if body else "GET")
293
+
294
+ try:
295
+ with opener.open(request, timeout=timeout) as response:
296
+ return response.read().decode("utf-8", "ignore")
297
+ except urllib.error.HTTPError as error: # type: ignore[attr-defined]
298
+ raise RuntimeError(f"Sillok request failed with HTTP {error.code} for {url}") from error
299
+ except urllib.error.URLError as error: # type: ignore[attr-defined]
300
+ raise RuntimeError(f"Sillok request failed for {url}: {error.reason}") from error
301
+
302
+
303
+ def extract_hidden_int(html_text: str, field: str) -> int | None:
304
+ pattern = re.compile(HIDDEN_VALUE_TEMPLATE.format(field=re.escape(field)))
305
+ match = pattern.search(html_text)
306
+ if not match:
307
+ return None
308
+ digits = DIGITS_PATTERN.search(match.group(1))
309
+ return int(digits.group(0)) if digits else None
310
+
311
+
312
+ def parse_result_title_metadata(title: str) -> ResultTitleMetadata:
313
+ cleaned_title = clean_text(title)
314
+ article_title = cleaned_title.split("/", 1)[1].strip() if "/" in cleaned_title else cleaned_title
315
+ metadata_match = re.search(r",\s*([^,]+?)\s+(즉위년|\d+년)\b", cleaned_title)
316
+
317
+ if not metadata_match:
318
+ return ResultTitleMetadata(None, None, None, article_title)
319
+
320
+ king = normalize_king_name(metadata_match.group(1))
321
+ year_token = metadata_match.group(2)
322
+ regnal_year = 1 if year_token == "즉위년" else int(year_token.removesuffix("년"))
323
+ accession_year = KING_ACCESSION_YEARS.get(king or "")
324
+ if accession_year is None:
325
+ gregorian_year = None
326
+ elif year_token == "즉위년":
327
+ gregorian_year = accession_year
328
+ else:
329
+ gregorian_year = accession_year + regnal_year
330
+
331
+ return ResultTitleMetadata(king, regnal_year, gregorian_year, article_title)
332
+
333
+
334
+ def parse_search_results(html_text: str, *, query: str, search_type: str) -> SearchReport:
335
+ total_results = extract_hidden_int(html_text, "totalCount")
336
+ if total_results is None:
337
+ total_match = TOTAL_COUNT_PATTERN.search(html_text)
338
+ total_results = int(total_match.group(1)) if total_match else 0
339
+
340
+ count_field = {"k": "countK", "w": "countW", "m": "countM", "c": "countC"}.get(search_type, "")
341
+ type_count = extract_hidden_int(html_text, count_field) if count_field else None
342
+ if type_count is None:
343
+ type_count = total_results
344
+
345
+ categories: list[SearchCategory] = []
346
+ for token, label_html in CATEGORY_PATTERN.findall(html_text):
347
+ label = clean_text(label_html)
348
+ match = re.match(r"(.+?)\s*\((\d+)\)", label)
349
+ if not match:
350
+ continue
351
+ categories.append(SearchCategory(label=match.group(1), count=int(match.group(2)), token=token))
352
+
353
+ items: list[SearchResult] = []
354
+ for article_id, subject_html, summary_html in RESULT_PATTERN.findall(html_text):
355
+ title = clean_text(subject_html)
356
+ title = re.sub(r"^\d+\.\s*", "", title)
357
+ metadata = parse_result_title_metadata(title)
358
+ items.append(
359
+ SearchResult(
360
+ article_id=article_id,
361
+ url=DETAIL_URL_TEMPLATE.format(article_id=article_id),
362
+ title=title,
363
+ article_title=metadata.article_title,
364
+ summary=clean_text(summary_html),
365
+ king=metadata.king,
366
+ regnal_year=metadata.regnal_year,
367
+ gregorian_year=metadata.gregorian_year,
368
+ )
369
+ )
370
+
371
+ return SearchReport(
372
+ query=query,
373
+ search_type=search_type,
374
+ total_results=total_results,
375
+ type_count=type_count,
376
+ categories=categories,
377
+ items=items,
378
+ )
379
+
380
+
381
+ def filter_results(
382
+ items: Iterable[SearchResult],
383
+ *,
384
+ king: str | None = None,
385
+ year: int | None = None,
386
+ ) -> list[SearchResult]:
387
+ normalized_king = normalize_king_name(king)
388
+ filtered: list[SearchResult] = []
389
+
390
+ for item in items:
391
+ if normalized_king is not None and item.king != normalized_king:
392
+ continue
393
+ if year is not None and item.gregorian_year != year:
394
+ continue
395
+ filtered.append(item)
396
+
397
+ return filtered
398
+
399
+
400
+ def parse_detail_page(html_text: str, *, article_id: str) -> ArticleDetail:
401
+ title_head = TITLE_HEAD_PATTERN.search(html_text)
402
+ if not title_head:
403
+ raise ValueError("Unable to find the article header on the detail page.")
404
+
405
+ translated_match = LEFT_VIEW_PATTERN.search(html_text)
406
+ original_match = RIGHT_VIEW_PATTERN.search(html_text)
407
+ if not translated_match or not original_match:
408
+ raise ValueError("Unable to find translated/original article text on the detail page.")
409
+
410
+ classification_match = CLASSIFICATION_PATTERN.search(html_text)
411
+
412
+ return ArticleDetail(
413
+ article_id=article_id,
414
+ url=DETAIL_URL_TEMPLATE.format(article_id=article_id),
415
+ header=clean_text(title_head.group(1)),
416
+ title=clean_text(title_head.group(2)),
417
+ translated_text=clean_article_text(translated_match.group(1)),
418
+ original_text=clean_article_text(original_match.group(1)),
419
+ classification=clean_text(classification_match.group(1)) if classification_match else None,
420
+ )
421
+
422
+
423
+ def build_search_payload(*, query: str, search_type: str, page_index: int) -> dict[str, str]:
424
+ return {
425
+ "topSearchWord": query,
426
+ "pageIndex": str(page_index),
427
+ "initPageUnit": "0",
428
+ "type": search_type,
429
+ "sillokType": "S",
430
+ "topSearchWord_ime": f'<span class="newbatang">{html.escape(query)}</span>',
431
+ }
432
+
433
+
434
+ def fetch_search_page(
435
+ opener: urllib.request.OpenerDirector,
436
+ *,
437
+ query: str,
438
+ search_type: str,
439
+ page_index: int,
440
+ timeout: int,
441
+ ) -> SearchReport:
442
+ payload = build_search_payload(query=query, search_type=search_type, page_index=page_index)
443
+ html_text = fetch_text(opener, SEARCH_URL, data=payload, timeout=timeout, referer=f"{BASE_URL}/main/main.do")
444
+ return parse_search_results(html_text, query=query, search_type=search_type)
445
+
446
+
447
+ def fetch_detail_page(
448
+ opener: urllib.request.OpenerDirector,
449
+ *,
450
+ article_id: str,
451
+ timeout: int,
452
+ ) -> ArticleDetail:
453
+ url = DETAIL_URL_TEMPLATE.format(article_id=article_id)
454
+ html_text = fetch_text(opener, url, timeout=timeout, referer=SEARCH_URL)
455
+ return parse_detail_page(html_text, article_id=article_id)
456
+
457
+
458
+ def search_sillok(
459
+ query: str,
460
+ *,
461
+ king: str | None = None,
462
+ year: int | None = None,
463
+ limit: int = DEFAULT_LIMIT,
464
+ search_type: str = "k",
465
+ timeout: int = DEFAULT_TIMEOUT,
466
+ ) -> dict:
467
+ opener = build_http_client()
468
+ reports: list[SearchReport] = []
469
+ filtered_results: list[SearchResult] = []
470
+
471
+ page_index = 1
472
+ total_pages = 1
473
+ while page_index <= total_pages and page_index <= MAX_PAGES:
474
+ report = fetch_search_page(opener, query=query, search_type=search_type, page_index=page_index, timeout=timeout)
475
+ reports.append(report)
476
+ page_filtered = filter_results(report.items, king=king, year=year)
477
+ filtered_results.extend(page_filtered)
478
+
479
+ if page_index == 1:
480
+ page_size = len(report.items) or 1
481
+ total_pages = max(1, math.ceil((report.type_count or report.total_results or 0) / page_size))
482
+ if len(filtered_results) >= limit or not report.items:
483
+ break
484
+ page_index += 1
485
+
486
+ first_report = reports[0] if reports else SearchReport(query, search_type, 0, 0, [], [])
487
+ limited_results = filtered_results[:limit]
488
+ details = [fetch_detail_page(opener, article_id=item.article_id, timeout=timeout) for item in limited_results]
489
+ detail_map = {detail.article_id: detail for detail in details}
490
+
491
+ serialized_items = []
492
+ for item in limited_results:
493
+ detail = detail_map.get(item.article_id)
494
+ serialized_items.append(
495
+ {
496
+ **asdict(item),
497
+ "detail": asdict(detail) if detail else None,
498
+ "excerpt": detail.translated_text[:280] if detail and detail.translated_text else item.summary[:280],
499
+ }
500
+ )
501
+
502
+ return {
503
+ "query": query,
504
+ "type": search_type,
505
+ "filters": {"king": normalize_king_name(king), "year": year, "limit": limit},
506
+ "total_results": first_report.total_results,
507
+ "type_count": first_report.type_count,
508
+ "returned_count": len(serialized_items),
509
+ "categories": [asdict(category) for category in first_report.categories],
510
+ "items": serialized_items,
511
+ }
512
+
513
+
514
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
515
+ parser = argparse.ArgumentParser(description="Search Joseon Sillok records from sillok.history.go.kr")
516
+ parser.add_argument("--query", required=True, help="Search keyword to send to the Joseon Sillok site")
517
+ parser.add_argument("--king", help="Optional king filter, e.g. 세종 or 세종실록")
518
+ parser.add_argument("--year", type=positive_int, help="Optional Gregorian year filter, e.g. 1443")
519
+ parser.add_argument("--limit", type=positive_int, default=DEFAULT_LIMIT, help="Number of results to return")
520
+ parser.add_argument(
521
+ "--type",
522
+ dest="search_type",
523
+ choices=["k", "w"],
524
+ default="k",
525
+ help="Search translated text (k) or original text (w)",
526
+ )
527
+ parser.add_argument("--timeout", type=positive_int, default=DEFAULT_TIMEOUT, help="HTTP timeout in seconds")
528
+ return parser.parse_args(argv)
529
+
530
+
531
+ def main(argv: list[str] | None = None) -> int:
532
+ args = parse_args(argv)
533
+
534
+ try:
535
+ report = search_sillok(
536
+ args.query,
537
+ king=args.king,
538
+ year=args.year,
539
+ limit=args.limit,
540
+ search_type=args.search_type,
541
+ timeout=args.timeout,
542
+ )
543
+ except Exception as error: # noqa: BLE001
544
+ print(json.dumps({"error": str(error)}, ensure_ascii=False, indent=2), file=sys.stderr)
545
+ return 1
546
+
547
+ print(json.dumps(report, ensure_ascii=False, indent=2))
548
+ return 0
549
+
550
+
551
+ if __name__ == "__main__":
552
+ raise SystemExit(main())
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "joseon-sillok-search",
3
+ "description": "Search Joseon Dynasty Annals records from the official sillok.history.go.kr site with keyword search plus optional king/year narrowing.",
4
+ "profiles": [
5
+ "lookup"
6
+ ],
7
+ "frontmatter": "name: joseon-sillok-search\ndescription: Search Joseon Dynasty Annals records from the official sillok.history.go.kr site with keyword search plus optional king/year narrowing.\nlicense: MIT\nmetadata:\n category: history\n locale: ko-KR\n phase: v1"
8
+ }