@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,523 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import re
6
+ import sys
7
+ import time
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ from dataclasses import asdict, dataclass
12
+ from html import unescape
13
+ from pathlib import Path
14
+ from typing import Callable
15
+
16
+ DEFAULT_RESULTS_URL = "https://nara-speller.co.kr/old_speller/results"
17
+ DEFAULT_MAX_CHARS = 1500
18
+ DEFAULT_TIMEOUT = 30
19
+ DEFAULT_THROTTLE_SECONDS = 1.2
20
+ RESULT_PAYLOAD_PATTERN = re.compile(r"data\s*=\s*(\[[\s\S]*?\]);\s*pageIdx\s*=")
21
+ NO_ISSUES_PATTERN = re.compile(r"맞춤법과\s*문법\s*오류를\s*찾지\s*못했습니다", re.MULTILINE)
22
+ TAG_PATTERN = re.compile(r"<[^>]+>")
23
+ LINE_BREAK_PATTERN = re.compile(r"<br\s*/?>", re.IGNORECASE)
24
+ SENTENCE_BOUNDARY_PATTERN = re.compile(r"(?<=[.!?。!?])\s+")
25
+ PARAGRAPH_SEPARATOR_PATTERN = re.compile(r"\n(?:[ \t]*\n)+")
26
+
27
+ DEFAULT_HEADERS = {
28
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
29
+ "Accept-Language": "ko,en-US;q=0.9,en;q=0.8",
30
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
31
+ "Origin": "https://nara-speller.co.kr",
32
+ "Referer": "https://nara-speller.co.kr/old_speller/",
33
+ "User-Agent": (
34
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
35
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
36
+ ),
37
+ }
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class SpellCheckIssue:
42
+ chunk_index: int
43
+ page_index: int
44
+ issue_index: int
45
+ sentence: str
46
+ original: str
47
+ suggestions: list[str]
48
+ reason: str
49
+ start: int | None
50
+ end: int | None
51
+ correct_method: int | None
52
+ error_message: str
53
+
54
+
55
+ def strip_html(value: str | None) -> str:
56
+ text = LINE_BREAK_PATTERN.sub("\n", value or "")
57
+ text = TAG_PATTERN.sub("", text)
58
+ return unescape(text).strip()
59
+
60
+
61
+ def split_candidates(value: str | None) -> list[str]:
62
+ return [candidate.strip() for candidate in str(value or "").split("|") if candidate.strip()]
63
+
64
+
65
+ def parse_positive_int(raw_value: str) -> int:
66
+ value = int(raw_value)
67
+ if value <= 0:
68
+ raise argparse.ArgumentTypeError("must be a positive integer")
69
+ return value
70
+
71
+
72
+ def split_text_into_chunks(text: str, max_chars: int = DEFAULT_MAX_CHARS) -> list[str]:
73
+ original = str(text or "")
74
+ if not original.strip():
75
+ return []
76
+
77
+ units = split_paragraph_units(original)
78
+ chunks: list[str] = []
79
+ current = ""
80
+
81
+ for unit in units:
82
+ candidate = unit if not current else f"{current}{unit}"
83
+
84
+ if len(candidate) <= max_chars:
85
+ current = candidate
86
+ continue
87
+
88
+ if current:
89
+ chunks.append(current)
90
+ current = ""
91
+
92
+ if len(unit) <= max_chars:
93
+ current = unit
94
+ continue
95
+
96
+ separator = ""
97
+ body = unit
98
+ separator_match = PARAGRAPH_SEPARATOR_PATTERN.search(unit)
99
+
100
+ if separator_match and separator_match.end() == len(unit):
101
+ separator = separator_match.group(0)
102
+ body = unit[: separator_match.start()]
103
+
104
+ for sentence in split_long_paragraph(body, max_chars=max_chars):
105
+ if len(sentence) <= max_chars:
106
+ chunks.append(sentence)
107
+ continue
108
+
109
+ start = 0
110
+ while start < len(sentence):
111
+ chunks.append(sentence[start : start + max_chars])
112
+ start += max_chars
113
+
114
+ if separator:
115
+ if chunks and len(chunks[-1]) + len(separator) <= max_chars:
116
+ chunks[-1] += separator
117
+ else:
118
+ current = separator
119
+
120
+ if current:
121
+ chunks.append(current)
122
+
123
+ return chunks
124
+
125
+
126
+ def split_paragraph_units(text: str) -> list[str]:
127
+ units: list[str] = []
128
+ start = 0
129
+
130
+ for match in PARAGRAPH_SEPARATOR_PATTERN.finditer(text):
131
+ paragraph = text[start : match.start()]
132
+ separator = match.group(0)
133
+
134
+ if paragraph:
135
+ units.append(paragraph + separator)
136
+ elif units:
137
+ units[-1] += separator
138
+ else:
139
+ units.append(separator)
140
+
141
+ start = match.end()
142
+
143
+ tail = text[start:]
144
+ if tail:
145
+ units.append(tail)
146
+
147
+ return units
148
+
149
+
150
+ def split_long_paragraph(paragraph: str, *, max_chars: int) -> list[str]:
151
+ sentence_boundaries = list(SENTENCE_BOUNDARY_PATTERN.finditer(paragraph))
152
+
153
+ if not sentence_boundaries:
154
+ return [paragraph]
155
+
156
+ sentences: list[str] = []
157
+ start = 0
158
+
159
+ for boundary in sentence_boundaries:
160
+ sentences.append(paragraph[start : boundary.end()])
161
+ start = boundary.end()
162
+
163
+ if start < len(paragraph):
164
+ sentences.append(paragraph[start:])
165
+
166
+ groups: list[str] = []
167
+ current = ""
168
+
169
+ for sentence in sentences:
170
+ candidate = sentence if not current else f"{current}{sentence}"
171
+
172
+ if len(candidate) <= max_chars:
173
+ current = candidate
174
+ continue
175
+
176
+ if current:
177
+ groups.append(current)
178
+ current = sentence
179
+
180
+ if current:
181
+ groups.append(current)
182
+
183
+ return groups
184
+
185
+
186
+ def fetch_spell_check_html(
187
+ text: str,
188
+ *,
189
+ strong_rules: bool = True,
190
+ timeout: int = DEFAULT_TIMEOUT,
191
+ url: str = DEFAULT_RESULTS_URL,
192
+ ) -> str:
193
+ body = {
194
+ "text1": text,
195
+ "chkKey": "",
196
+ }
197
+
198
+ if strong_rules:
199
+ body["btnModeChange"] = "on"
200
+
201
+ request = urllib.request.Request(
202
+ url,
203
+ data=urllib.parse.urlencode(body).encode("utf-8"),
204
+ headers=DEFAULT_HEADERS,
205
+ method="POST",
206
+ )
207
+
208
+ try:
209
+ with urllib.request.urlopen(request, timeout=timeout) as response:
210
+ return response.read().decode("utf-8", "ignore")
211
+ except urllib.error.HTTPError as error: # type: ignore[attr-defined]
212
+ if error.code == 403:
213
+ raise RuntimeError(
214
+ "The spell-check service returned HTTP 403. "
215
+ "This environment may be hitting a Cloudflare/browser challenge. "
216
+ "Retry later with lower request volume or from a browser-friendly network."
217
+ ) from error
218
+
219
+ raise RuntimeError(f"The spell-check service returned HTTP {error.code}.") from error
220
+
221
+
222
+ def extract_result_payload(html: str) -> list[dict]:
223
+ match = RESULT_PAYLOAD_PATTERN.search(html)
224
+
225
+ if not match:
226
+ if NO_ISSUES_PATTERN.search(html):
227
+ return []
228
+ raise ValueError("Unable to find the spell-check payload in the returned HTML.")
229
+
230
+ payload = json.loads(match.group(1))
231
+
232
+ if not isinstance(payload, list):
233
+ raise ValueError("The extracted spell-check payload was not a list.")
234
+
235
+ return payload
236
+
237
+
238
+ def apply_page_corrections(page: dict) -> str:
239
+ source = str(page.get("str", ""))
240
+ corrected = source
241
+
242
+ for error in sorted(page.get("errInfo", []), key=lambda item: int(item.get("start", -1)), reverse=True):
243
+ suggestions = split_candidates(error.get("candWord"))
244
+ original = str(error.get("orgStr", ""))
245
+
246
+ if not suggestions:
247
+ continue
248
+
249
+ start = int(error.get("start", -1))
250
+ end = int(error.get("end", -1))
251
+
252
+ if start < 0 or end < start or end >= len(source):
253
+ continue
254
+
255
+ slice_end = end + 1
256
+ if original:
257
+ while slice_end > start and source[start:slice_end] != original and source[start : slice_end - 1] == original:
258
+ slice_end -= 1
259
+
260
+ corrected = f"{corrected[:start]}{suggestions[0]}{corrected[slice_end:]}"
261
+
262
+ return corrected
263
+
264
+
265
+ def build_visible_text_index(text: str) -> tuple[str, list[int], list[int | None]]:
266
+ visible_chars: list[str] = []
267
+ visible_indices: list[int] = []
268
+ visible_lookup: list[int | None] = []
269
+
270
+ for index, char in enumerate(text):
271
+ if char.isspace():
272
+ visible_lookup.append(None)
273
+ continue
274
+
275
+ visible_lookup.append(len(visible_indices))
276
+ visible_chars.append(char)
277
+ visible_indices.append(index)
278
+
279
+ return "".join(visible_chars), visible_indices, visible_lookup
280
+
281
+
282
+ def preserve_original_layout(original: str, suggestion: str) -> str:
283
+ if "\n" not in original:
284
+ return suggestion
285
+
286
+ original_visible, original_visible_indices, _ = build_visible_text_index(original)
287
+ suggestion_visible, suggestion_visible_indices, _ = build_visible_text_index(suggestion)
288
+
289
+ if original_visible != suggestion_visible:
290
+ return suggestion
291
+
292
+ if not original_visible_indices or not suggestion_visible_indices:
293
+ return original if original.strip() else suggestion
294
+
295
+ merged: list[str] = []
296
+ leading_original = original[: original_visible_indices[0]]
297
+ leading_suggestion = suggestion[: suggestion_visible_indices[0]]
298
+ merged.append(leading_original if leading_original.isspace() else leading_suggestion)
299
+
300
+ for ordinal, suggestion_index in enumerate(suggestion_visible_indices):
301
+ merged.append(suggestion[suggestion_index])
302
+
303
+ next_original_index = original_visible_indices[ordinal + 1] if ordinal + 1 < len(original_visible_indices) else None
304
+ next_suggestion_index = (
305
+ suggestion_visible_indices[ordinal + 1] if ordinal + 1 < len(suggestion_visible_indices) else None
306
+ )
307
+
308
+ original_gap = (
309
+ original[original_visible_indices[ordinal] + 1 : next_original_index]
310
+ if next_original_index is not None
311
+ else original[original_visible_indices[ordinal] + 1 :]
312
+ )
313
+ suggestion_gap = (
314
+ suggestion[suggestion_index + 1 : next_suggestion_index]
315
+ if next_suggestion_index is not None
316
+ else suggestion[suggestion_index + 1 :]
317
+ )
318
+
319
+ merged.append(original_gap if "\n" in original_gap else suggestion_gap)
320
+
321
+ return "".join(merged)
322
+
323
+
324
+ def apply_chunk_corrections(chunk: str, pages: list[dict]) -> str:
325
+ combined_source = "".join(str(page.get("str", "")) for page in pages)
326
+ fallback = "".join(apply_page_corrections(page) for page in pages) or chunk
327
+
328
+ if not combined_source:
329
+ return fallback
330
+
331
+ chunk_visible, chunk_visible_indices, _ = build_visible_text_index(chunk)
332
+ source_visible, _, source_visible_lookup = build_visible_text_index(combined_source)
333
+
334
+ if chunk_visible != source_visible:
335
+ return fallback
336
+
337
+ replacements: list[tuple[int, int, str, str]] = []
338
+ page_offset = 0
339
+
340
+ for page in pages:
341
+ for error in page.get("errInfo", []):
342
+ suggestions = split_candidates(error.get("candWord"))
343
+ if not suggestions:
344
+ continue
345
+
346
+ start = int(error.get("start", -1))
347
+ end = int(error.get("end", -1))
348
+
349
+ if start < 0 or end < start:
350
+ continue
351
+
352
+ start += page_offset
353
+ end += page_offset
354
+
355
+ visible_ordinals = [
356
+ source_visible_lookup[index]
357
+ for index in range(start, min(end + 1, len(source_visible_lookup)))
358
+ if source_visible_lookup[index] is not None
359
+ ]
360
+
361
+ if not visible_ordinals:
362
+ continue
363
+
364
+ original_start = chunk_visible_indices[visible_ordinals[0]]
365
+ original_end = chunk_visible_indices[visible_ordinals[-1]]
366
+ replacements.append((original_start, original_end, suggestions[0], str(error.get("orgStr", ""))))
367
+
368
+ page_offset += len(str(page.get("str", "")))
369
+
370
+ if not replacements:
371
+ return chunk
372
+
373
+ corrected = chunk
374
+
375
+ for start, end, suggestion, original in sorted(replacements, key=lambda item: item[0], reverse=True):
376
+ slice_end = end + 1
377
+ if original:
378
+ while (
379
+ slice_end > start
380
+ and corrected[start:slice_end] != original
381
+ and corrected[start : slice_end - 1] == original
382
+ ):
383
+ slice_end -= 1
384
+
385
+ original_slice = corrected[start:slice_end]
386
+ replacement = preserve_original_layout(original_slice, suggestion)
387
+ corrected = f"{corrected[:start]}{replacement}{corrected[slice_end:]}"
388
+
389
+ return corrected
390
+
391
+
392
+ def build_issue(chunk_index: int, page_index: int, issue_index: int, page: dict, error: dict) -> SpellCheckIssue:
393
+ return SpellCheckIssue(
394
+ chunk_index=chunk_index,
395
+ page_index=page_index,
396
+ issue_index=issue_index,
397
+ sentence=str(page.get("str", "")),
398
+ original=str(error.get("orgStr", "")),
399
+ suggestions=split_candidates(error.get("candWord")),
400
+ reason=strip_html(error.get("help")) or strip_html(error.get("errMsg")),
401
+ start=int(error["start"]) if str(error.get("start", "")).strip() else None,
402
+ end=int(error["end"]) if str(error.get("end", "")).strip() else None,
403
+ correct_method=int(error["correctMethod"])
404
+ if str(error.get("correctMethod", "")).strip()
405
+ else None,
406
+ error_message=strip_html(error.get("errMsg")),
407
+ )
408
+
409
+
410
+ def check_text(
411
+ text: str,
412
+ *,
413
+ max_chars: int = DEFAULT_MAX_CHARS,
414
+ strong_rules: bool = True,
415
+ timeout: int = DEFAULT_TIMEOUT,
416
+ throttle_seconds: float = DEFAULT_THROTTLE_SECONDS,
417
+ requester: Callable[..., str] = fetch_spell_check_html,
418
+ sleep_fn: Callable[[float], None] = time.sleep,
419
+ ) -> dict:
420
+ chunks = split_text_into_chunks(text, max_chars=max_chars)
421
+ corrected_chunks: list[str] = []
422
+ issues: list[SpellCheckIssue] = []
423
+ chunk_reports: list[dict] = []
424
+
425
+ for chunk_index, chunk in enumerate(chunks):
426
+ if chunk_index > 0 and throttle_seconds > 0:
427
+ sleep_fn(throttle_seconds)
428
+
429
+ html = requester(chunk, strong_rules=strong_rules, timeout=timeout)
430
+ pages = extract_result_payload(html)
431
+ corrected_chunk = apply_chunk_corrections(chunk, pages)
432
+
433
+ corrected_chunks.append(corrected_chunk)
434
+ chunk_reports.append(
435
+ {
436
+ "chunk_index": chunk_index,
437
+ "original_text": chunk,
438
+ "corrected_text": corrected_chunk,
439
+ "page_count": len(pages),
440
+ }
441
+ )
442
+
443
+ for page_index, page in enumerate(pages):
444
+ for issue_index, error in enumerate(page.get("errInfo", [])):
445
+ issues.append(build_issue(chunk_index, page_index, issue_index, page, error))
446
+
447
+ return {
448
+ "original_text": str(text or ""),
449
+ "corrected_text": "".join(corrected_chunks),
450
+ "chunks": chunk_reports,
451
+ "issues": issues,
452
+ "meta": {
453
+ "chunk_count": len(chunks),
454
+ "strong_rules": strong_rules,
455
+ "max_chars": max_chars,
456
+ },
457
+ }
458
+
459
+
460
+ def parse_args(argv: list[str]) -> argparse.Namespace:
461
+ parser = argparse.ArgumentParser(description="Run the official Nara/PNU Korean spell checker.")
462
+ parser.add_argument("--text", help="Inline Korean text to inspect.")
463
+ parser.add_argument("--file", help="UTF-8 text/markdown file to inspect.")
464
+ parser.add_argument("--max-chars", type=parse_positive_int, default=DEFAULT_MAX_CHARS)
465
+ parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
466
+ parser.add_argument("--throttle-seconds", type=float, default=DEFAULT_THROTTLE_SECONDS)
467
+ parser.add_argument("--weak-rules", action="store_true", help="Disable the strong-rules checkbox.")
468
+ parser.add_argument("--format", choices=["json", "text"], default="json")
469
+ args = parser.parse_args(argv)
470
+
471
+ if not args.text and not args.file:
472
+ parser.error("Either --text or --file is required.")
473
+
474
+ return args
475
+
476
+
477
+ def load_input(args: argparse.Namespace) -> str:
478
+ if args.text:
479
+ return args.text
480
+
481
+ return Path(args.file).read_text(encoding="utf-8")
482
+
483
+
484
+ def serialize_report(report: dict) -> dict:
485
+ return {
486
+ **report,
487
+ "issues": [asdict(issue) for issue in report["issues"]],
488
+ }
489
+
490
+
491
+ def print_text_report(report: dict) -> None:
492
+ print("# corrected_text")
493
+ print(report["corrected_text"])
494
+ print()
495
+ print("# issues")
496
+
497
+ for issue in report["issues"]:
498
+ print(f"- chunk={issue.chunk_index} page={issue.page_index} issue={issue.issue_index}")
499
+ print(f" original: {issue.original}")
500
+ print(f" suggestions: {', '.join(issue.suggestions) if issue.suggestions else '(없음)'}")
501
+ print(f" reason: {issue.reason or '(없음)'}")
502
+
503
+
504
+ def main(argv: list[str] | None = None) -> int:
505
+ args = parse_args(argv or sys.argv[1:])
506
+ report = check_text(
507
+ load_input(args),
508
+ max_chars=args.max_chars,
509
+ strong_rules=not args.weak_rules,
510
+ timeout=args.timeout,
511
+ throttle_seconds=args.throttle_seconds,
512
+ )
513
+
514
+ if args.format == "json":
515
+ print(json.dumps(serialize_report(report), ensure_ascii=False, indent=2))
516
+ else:
517
+ print_text_report(report)
518
+
519
+ return 0
520
+
521
+
522
+ if __name__ == "__main__":
523
+ raise SystemExit(main())
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "korean-spell-check",
3
+ "description": "Use Nara/PNU Korean spell-check surfaces conservatively to proofread Korean text, chunk long input, and return change-focused correction suggestions.",
4
+ "profiles": [
5
+ "browser",
6
+ "local"
7
+ ],
8
+ "frontmatter": "name: korean-spell-check\ndescription: Use Nara/PNU Korean spell-check surfaces conservatively to proofread Korean text, chunk long input, and return change-focused correction suggestions.\nlicense: MIT\nmetadata:\n category: writing\n locale: ko-KR\n phase: v1"
9
+ }