@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,224 @@
1
+ import importlib.util
2
+ import json
3
+ import subprocess
4
+ import sys
5
+ from argparse import Namespace
6
+ from datetime import date, datetime, timezone
7
+ from pathlib import Path
8
+ import unittest
9
+ from unittest import mock
10
+
11
+
12
+ SCRIPT_DIR = Path(__file__).resolve().parent
13
+ FILTER_PATH = SCRIPT_DIR / "scholarship_filter.py"
14
+ PLANNER_PATH = SCRIPT_DIR / "university_search_plan.py"
15
+
16
+
17
+ def load_module(module_name: str, path: Path):
18
+ spec = importlib.util.spec_from_file_location(module_name, path)
19
+ module = importlib.util.module_from_spec(spec)
20
+ assert spec.loader is not None
21
+ spec.loader.exec_module(module)
22
+ return module
23
+
24
+
25
+ scholarship_filter = load_module("scholarship_filter", FILTER_PATH)
26
+ university_search_plan = load_module("university_search_plan", PLANNER_PATH)
27
+
28
+
29
+ class DeadlineStatusTest(unittest.TestCase):
30
+ def test_current_kst_date_uses_korea_calendar_day(self):
31
+ now = datetime(2026, 4, 15, 16, 30, tzinfo=timezone.utc)
32
+
33
+ today = scholarship_filter.current_kst_date(now)
34
+
35
+ self.assertEqual(today, date(2026, 4, 16))
36
+
37
+ def test_resolve_today_falls_back_to_kst_when_missing_or_invalid(self):
38
+ with mock.patch.object(scholarship_filter, "current_kst_date", return_value=date(2026, 4, 16)):
39
+ self.assertEqual(scholarship_filter.resolve_today(None), date(2026, 4, 16))
40
+ self.assertEqual(scholarship_filter.resolve_today("not-a-date"), date(2026, 4, 16))
41
+
42
+ def test_infer_deadline_status_overrides_stale_cached_status_with_dates(self):
43
+ record = {
44
+ "deadline": {
45
+ "status": "open",
46
+ "start": "2026-04-01",
47
+ "end": "2026-04-14",
48
+ }
49
+ }
50
+
51
+ status = scholarship_filter.infer_deadline_status(record, date(2026, 4, 15))
52
+
53
+ self.assertEqual(status, "closed")
54
+
55
+ def test_infer_deadline_status_returns_unknown_for_noncanonical_cached_value_without_dates(self):
56
+ record = {"deadline": {"status": "d-3"}}
57
+
58
+ status = scholarship_filter.infer_deadline_status(record, date(2026, 4, 15))
59
+
60
+ self.assertEqual(status, "unknown")
61
+
62
+ def test_infer_deadline_status_treats_end_date_equal_to_today_as_open(self):
63
+ record = {"deadline": {"end": "2026-04-15"}}
64
+
65
+ status = scholarship_filter.infer_deadline_status(record, date(2026, 4, 15))
66
+
67
+ self.assertEqual(status, "open")
68
+
69
+ def test_report_does_not_crash_on_noncanonical_status_and_counts_unknown(self):
70
+ payload = json.dumps([{"name": "x", "deadline": {"status": "d-3"}}], ensure_ascii=False)
71
+
72
+ result = subprocess.run(
73
+ [sys.executable, str(FILTER_PATH), "report", "--today", "2026-04-15"],
74
+ input=payload,
75
+ text=True,
76
+ capture_output=True,
77
+ check=True,
78
+ )
79
+
80
+ self.assertIn("- 기준일: 2026-04-15 (Asia/Seoul (KST))", result.stdout)
81
+ self.assertIn("- 상태 미확인: 1", result.stdout)
82
+ self.assertIn("## 상태 미확인", result.stdout)
83
+
84
+
85
+ class AmountHandlingTest(unittest.TestCase):
86
+ def test_extract_amount_value_uses_amount_fields_and_ignores_irrelevant_notes(self):
87
+ from_text = scholarship_filter.extract_amount_value({"amount": {"text": "생활비 250만원 지급"}})
88
+ ignored_notes = scholarship_filter.extract_amount_value(
89
+ {
90
+ "amount": {"text": "등록금 전액"},
91
+ "notes": ["작년에는 500만원 특별지원"],
92
+ }
93
+ )
94
+
95
+ self.assertEqual(from_text, 2500000)
96
+ self.assertIsNone(ignored_notes)
97
+
98
+ def test_match_filter_keeps_text_only_amount_by_default_and_marks_it_unknown(self):
99
+ args = Namespace(
100
+ q=None,
101
+ org_type=None,
102
+ school_kind=None,
103
+ school_name=None,
104
+ student_level=None,
105
+ major=None,
106
+ department_name=None,
107
+ grade_year=None,
108
+ gpa=None,
109
+ income_band=None,
110
+ min_amount=2000000,
111
+ max_amount=None,
112
+ strict_amount=False,
113
+ deadline_status=None,
114
+ today="2026-04-15",
115
+ only_open_now=False,
116
+ upcoming_within_days=None,
117
+ deadline_within_days=None,
118
+ )
119
+
120
+ matched, reasons = scholarship_filter.match_filter({"amount": {"text": "등록금 전액"}}, args)
121
+
122
+ self.assertTrue(matched)
123
+ self.assertIn("amount>=2000000?", reasons)
124
+
125
+ def test_match_filter_can_drop_text_only_amount_in_strict_mode(self):
126
+ args = Namespace(
127
+ q=None,
128
+ org_type=None,
129
+ school_kind=None,
130
+ school_name=None,
131
+ student_level=None,
132
+ major=None,
133
+ department_name=None,
134
+ grade_year=None,
135
+ gpa=None,
136
+ income_band=None,
137
+ min_amount=2000000,
138
+ max_amount=None,
139
+ strict_amount=True,
140
+ deadline_status=None,
141
+ today="2026-04-15",
142
+ only_open_now=False,
143
+ upcoming_within_days=None,
144
+ deadline_within_days=None,
145
+ )
146
+
147
+ matched, reasons = scholarship_filter.match_filter({"amount": {"text": "등록금 전액"}}, args)
148
+
149
+ self.assertFalse(matched)
150
+ self.assertEqual(reasons, [])
151
+
152
+
153
+ class SparseFieldPolicyTest(unittest.TestCase):
154
+ def test_eligibility_returns_indeterminate_when_profile_fields_are_missing(self):
155
+ args = Namespace(
156
+ org_type=None,
157
+ school_kind="university",
158
+ school_name="서울대학교",
159
+ student_level="undergraduate",
160
+ major=None,
161
+ department_name=None,
162
+ grade_year=None,
163
+ gpa=None,
164
+ income_band=5,
165
+ )
166
+
167
+ result = scholarship_filter.eligibility_result({"name": "테스트 장학금"}, args)
168
+
169
+ self.assertEqual(result["status"], "indeterminate")
170
+ self.assertEqual(result["failed"], [])
171
+ self.assertEqual(
172
+ result["unknown"],
173
+ ["school_kind=?", "school_name=?", "student_level=?", "income_band=?"],
174
+ )
175
+
176
+ def test_school_name_filter_does_not_match_urls_any_more(self):
177
+ args = Namespace(
178
+ q=None,
179
+ org_type=None,
180
+ school_kind=None,
181
+ school_name="SNU",
182
+ student_level=None,
183
+ major=None,
184
+ department_name=None,
185
+ grade_year=None,
186
+ gpa=None,
187
+ income_band=None,
188
+ min_amount=None,
189
+ max_amount=None,
190
+ strict_amount=False,
191
+ deadline_status=None,
192
+ today="2026-04-15",
193
+ only_open_now=False,
194
+ upcoming_within_days=None,
195
+ deadline_within_days=None,
196
+ )
197
+ record = {
198
+ "organization": {"name": "한국장학재단"},
199
+ "source_url": "https://www.kosaf.go.kr/snu-notice",
200
+ }
201
+
202
+ matched, reasons = scholarship_filter.match_filter(record, args)
203
+
204
+ self.assertFalse(matched)
205
+ self.assertEqual(reasons, [])
206
+
207
+
208
+ class UniversitySearchPlanTest(unittest.TestCase):
209
+ def test_school_domain_suppresses_broad_ac_kr_fallback_queries(self):
210
+ payload = university_search_plan.build_school_queries(
211
+ school_name="서울대학교",
212
+ school_domain="snu.ac.kr",
213
+ departments=["컴퓨터공학부"],
214
+ colleges=[],
215
+ year=2026,
216
+ )
217
+
218
+ queries = payload["search_queries"]
219
+ self.assertTrue(any(query.startswith("site:snu.ac.kr ") for query in queries))
220
+ self.assertFalse(any('site:*.ac.kr "서울대학교"' in query for query in queries))
221
+
222
+
223
+ if __name__ == "__main__":
224
+ unittest.main()
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """Generate exhaustive scholarship search queries for any Korean university."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from datetime import date
9
+
10
+
11
+ def build_school_queries(
12
+ school_name: str,
13
+ school_domain: str | None,
14
+ departments: list[str],
15
+ colleges: list[str],
16
+ year: int,
17
+ ) -> dict[str, object]:
18
+ if school_domain:
19
+ domain_targets = [f"site:{school_domain}"]
20
+ else:
21
+ domain_targets = [f"site:*.ac.kr \"{school_name}\""]
22
+
23
+ base_suffixes = [
24
+ f"{year} 장학 공고",
25
+ f"{year} 교내 장학",
26
+ f"{year} 외부 장학",
27
+ f"{year} 학생지원처 장학",
28
+ f"{year} 학사공지 장학",
29
+ "장학 공고",
30
+ "교내 장학",
31
+ "외부 장학 추천",
32
+ "학생지원처 장학",
33
+ "학사공지 장학",
34
+ ]
35
+
36
+ queries: list[str] = []
37
+ for target in domain_targets:
38
+ for suffix in base_suffixes:
39
+ queries.append(f"{target} {suffix}")
40
+
41
+ for college in colleges:
42
+ queries.append(f"{target} \"{college}\" 장학")
43
+ queries.append(f"{target} \"{college}\" 외부 장학")
44
+ queries.append(f"{target} \"{college}\" 장학생 모집")
45
+
46
+ for department in departments:
47
+ queries.append(f"{target} \"{department}\" 장학")
48
+ queries.append(f"{target} \"{department}\" 외부 장학")
49
+ queries.append(f"{target} \"{department}\" 공지 장학생")
50
+ queries.append(f"{target} \"{department}\" 대학원 장학")
51
+
52
+ url_hints = [
53
+ "/scholarship",
54
+ "/student/scholarship",
55
+ "/notice",
56
+ "/bbs",
57
+ "/board",
58
+ "/undergraduate/notice",
59
+ "/graduate/notice",
60
+ "/academics/undergraduate/scholarship",
61
+ "/academics/graduate/scholarship",
62
+ ]
63
+
64
+ checklist = [
65
+ "학교 대표 장학공지",
66
+ "학생지원처 / 장학팀",
67
+ "학사공지 / 일반공지",
68
+ "단과대 공지",
69
+ "학과 / 전공 공지",
70
+ "대학원 공지",
71
+ "첨부 PDF/HWP",
72
+ ]
73
+
74
+ return {
75
+ "scope": "school",
76
+ "school_name": school_name,
77
+ "school_domain": school_domain,
78
+ "year": year,
79
+ "departments": departments,
80
+ "colleges": colleges,
81
+ "coverage_checklist": checklist,
82
+ "search_queries": queries,
83
+ "url_hints": url_hints,
84
+ }
85
+
86
+
87
+ def build_nationwide_queries(year: int) -> dict[str, object]:
88
+ queries = [
89
+ f"site:*.ac.kr {year} 장학 공고",
90
+ f"site:*.ac.kr {year} 교내 장학",
91
+ f"site:*.ac.kr {year} 외부 장학 추천",
92
+ f"site:*.ac.kr {year} 학과 장학",
93
+ f"site:*.ac.kr {year} 대학원 장학",
94
+ "site:*.ac.kr 장학 공고",
95
+ "site:*.ac.kr 교내 장학",
96
+ "site:*.ac.kr 외부 장학 추천",
97
+ "site:*.ac.kr 학과 장학",
98
+ "site:*.ac.kr 대학원 장학",
99
+ ]
100
+ return {
101
+ "scope": "nationwide-universities",
102
+ "year": year,
103
+ "search_queries": queries,
104
+ "coverage_notes": [
105
+ "공개된 *.ac.kr 장학 공지 중심",
106
+ "학교 본부 -> 단과대 -> 학과 순서로 수집",
107
+ "검색엔진에 노출되지 않은 게시판은 누락 가능",
108
+ "첨부 PDF/HWP 확인 필요",
109
+ ],
110
+ }
111
+
112
+
113
+ def build_parser() -> argparse.ArgumentParser:
114
+ parser = argparse.ArgumentParser(
115
+ description="Generate exhaustive official scholarship search queries for a Korean university or for nationwide university coverage.",
116
+ )
117
+ parser.add_argument("--school-name", help="University name, e.g. 서울대학교.")
118
+ parser.add_argument("--school-domain", help="Official university domain, e.g. snu.ac.kr.")
119
+ parser.add_argument("--department", action="append", default=[], help="Department or program name. Repeatable.")
120
+ parser.add_argument("--college", action="append", default=[], help="College/faculty name. Repeatable.")
121
+ parser.add_argument("--nationwide", action="store_true", help="Generate search queries for all Korean universities.")
122
+ parser.add_argument("--year", type=int, default=date.today().year, help="Target year for notice search.")
123
+ return parser
124
+
125
+
126
+ def main() -> int:
127
+ parser = build_parser()
128
+ args = parser.parse_args()
129
+
130
+ if args.nationwide:
131
+ payload = build_nationwide_queries(args.year)
132
+ else:
133
+ if not args.school_name:
134
+ raise SystemExit("--school-name is required unless --nationwide is used")
135
+ payload = build_school_queries(
136
+ school_name=args.school_name,
137
+ school_domain=args.school_domain,
138
+ departments=args.department,
139
+ colleges=args.college,
140
+ year=args.year,
141
+ )
142
+
143
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
144
+ return 0
145
+
146
+
147
+ if __name__ == "__main__":
148
+ raise SystemExit(main())
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "korean-scholarship-search",
3
+ "description": "Search Korean scholarship announcements across official KOSAF, university, foundation, company, and public-sector sources, extract amount and eligibility, and filter results by school, income band, student level, and organization type. Users may invoke it with the phrase 장학금 검색 및 조회. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
4
+ "profiles": [
5
+ "action:submission"
6
+ ],
7
+ "frontmatter": "name: korean-scholarship-search\ndescription: Search Korean scholarship announcements across official KOSAF, university, foundation, company, and public-sector sources, extract amount and eligibility, and filter results by school, income band, student level, and organization type. Users may invoke it with the phrase 장학금 검색 및 조회. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: education\n locale: ko-KR\n phase: v1"
8
+ }
@@ -0,0 +1,181 @@
1
+ # Korean Slang Writing
2
+
3
+ ## What this skill does
4
+
5
+ 사용자가 "유행어 섞어서", "요즘 말투로", "밈스럽게", "재치있게" 같은 요청을 했을 때, **검증된 한국 유행어 후보**를 먼저 뽑고 필요하면 나무위키 원문 요약을 확인해 한국어 문장·홍보문·SNS 게시글·댓글을 유행어 느낌으로 작성한다.
6
+
7
+ - 스킬은 **데이터와 조회 도구**만 제공한다: 큐레이션된 시드 인덱스 검색 + 나무위키 best-effort 요약.
8
+ - 실제 문장 작성은 **에이전트(LLM)** 가 스킬이 돌려준 후보 중에서 사용자 의도에 맞는 표현을 골라 한다.
9
+ - 시드 인덱스 출처는 [나무위키 분류:유행어](https://namu.wiki/w/%EB%B6%84%EB%A5%98:%EC%9C%A0%ED%96%89%EC%96%B4) 를 기준으로 수작업 큐레이션한다.
10
+ - risky 수준의 혐오·차별·집단 조롱 가능성이 있는 표현은 시드에 넣지 않는다.
11
+
12
+ ## When to use
13
+
14
+ - "SNS 홍보 글 유행어 섞어서 써줘"
15
+ - "댓글을 요즘 말투로 바꿔줘"
16
+ - "밈스럽게 / MZ스럽게 / 재치있게 써줘"
17
+ - "이 유행어가 무슨 뜻이야?" (시드 인덱스 + 나무위키 lookup)
18
+ - "긍정적인 느낌의 유행어 3개 추천해줘"
19
+ - "이 상품 카피에 어울리는 유행어 후보 뽑아줘"
20
+
21
+ ## When NOT to use
22
+
23
+ - 격식 공문, 보도자료, 법률 문서, 사과문, 공식 발표문 등 유행어 사용이 부적절한 문맥
24
+ - 민감한 사회·정치·종교 주제에 풍자 유행어를 끼얹어야 하는 상황
25
+ - 대량 인덱싱 / 전체 웹 스크래핑 (나무위키 약관·Cloudflare 정책 위반 소지)
26
+ - 불특정 다수 대상의 브랜딩 문구에서 유행어 과다 사용 (저자가 확인·동의하지 않은 경우)
27
+
28
+ ## Policy first
29
+
30
+ - 유행어는 **자연스럽게 적게** 쓰는 것을 기본값으로 한다. 과다 사용은 오히려 촌스럽다.
31
+ - 뜻과 용례를 확인하지 못한 유행어는 사용하지 않는다.
32
+ - `safety` 가 `spicy` 인 항목은 비격식 컨텍스트에서만 쓰고, 격식 자리에는 피한다.
33
+ - `still_usable: false` 표시가 붙은 구형 유행어는 기본 검색 결과에 포함되지 않는다 (`--include-deprecated` 로 의도적 복원 가능).
34
+ - 나무위키는 best-effort 출처이며 Cloudflare 차단·HTML 구조 변경 시 `fetched: false` 또는 `warning` 으로 응답한다. 이 경우 추측하지 말고 시드 인덱스 meaning_short + 링크만 안내한다.
35
+
36
+ ## Prerequisites
37
+
38
+ - `python3` 3.10+ (`argparse`, `urllib`, `unittest` 만 사용 — 추가 의존성 없음)
39
+ - 스킬 디렉터리의 `scripts/*.py` helper (설치 시 자동 포함)
40
+ - 나무위키 lookup 에는 인터넷 연결이 필요하지만, 실패해도 시드 인덱스만으로 안전하게 응답할 수 있다.
41
+
42
+ ## Response contract
43
+
44
+ `slang_search` 결과 스키마 (v1):
45
+
46
+ ```json
47
+ {
48
+ "query": "중꺾마",
49
+ "filters_applied": {
50
+ "mood": ["긍정"],
51
+ "context": ["SNS"],
52
+ "safety": ["safe"],
53
+ "intensity": [],
54
+ "limit": 10,
55
+ "include_deprecated": false
56
+ },
57
+ "matched_before_limit": 12,
58
+ "total_candidates": 5,
59
+ "candidates": [
60
+ {
61
+ "term": "중꺾마",
62
+ "aliases": ["중요한 건 꺾이지 않는 마음"],
63
+ "meaning_short": "포기하지 않는 불굴의 의지.",
64
+ "usage_context": ["격려", "스포츠", "SNS"],
65
+ "mood_tags": ["긍정", "의지"],
66
+ "intensity": "medium",
67
+ "safety": "safe",
68
+ "example_usage": ["힘들지만 중꺾마 정신으로 이겨내자!"],
69
+ "namuwiki_url": "https://namu.wiki/w/...",
70
+ "era": "2022",
71
+ "still_usable": true,
72
+ "match_reason": "exact"
73
+ }
74
+ ],
75
+ "source": "...",
76
+ "last_reviewed": "2026-04-22"
77
+ }
78
+ ```
79
+
80
+ `match_reason` 우선순위: `exact` > `alias` > `substring` > `no-query` (필터만 적용한 경우).
81
+
82
+ `slang_lookup` 결과 스키마:
83
+
84
+ ```json
85
+ {
86
+ "input": "중꺾마",
87
+ "url": "https://namu.wiki/w/%EC%A4%91%EA%BA%BE%EB%A7%88",
88
+ "fetched": true,
89
+ "title": "중꺾마",
90
+ "summary": "중꺾마는 ...",
91
+ "error": null,
92
+ "block_reason": null,
93
+ "warning": "optional, when HTML parsing failed"
94
+ }
95
+ ```
96
+
97
+ `fetched: false` 인 경우 `block_reason` 이 `blocked` / `not_found` / `upstream_error` 중 하나이며, 에이전트는 추측하지 않고 원문 링크를 제시하거나 다른 출처 확인을 제안한다.
98
+
99
+ ## Workflow
100
+
101
+ ### 1. 사용자 의도 정리
102
+
103
+ - 글쓰기 목적 (홍보, 댓글, 자기소개, 제목 등)
104
+ - 유행어 강도 (살짝 / 적당히 / 많이)
105
+ - 톤/무드 (긍정, 부정, 유머, 자조, 감탄 등)
106
+ - 문맥 (SNS, 마케팅, 음식, 스포츠, 직장, 학교 등)
107
+
108
+ ### 2. 시드 인덱스에서 후보 탐색
109
+
110
+ ```bash
111
+ python3 scripts/slang_search.py --mood "긍정,유머" --context "SNS,마케팅" --safety safe --limit 5 --format json
112
+ ```
113
+
114
+ 주요 옵션:
115
+
116
+ | 옵션 | 설명 | 예 |
117
+ |------|------|-----|
118
+ | `--query` | 키워드 매칭 (term/aliases/substring) | `--query 중꺾마` |
119
+ | `--mood` | 무드 태그 (OR 매칭) | `--mood 긍정,유머` |
120
+ | `--context` | 문맥 태그 (OR 매칭) | `--context SNS,마케팅` |
121
+ | `--safety` | 안전 수준 (safe/spicy/risky) | `--safety safe,spicy` |
122
+ | `--intensity` | 강도 (subtle/medium/strong) | `--intensity medium,strong` |
123
+ | `--limit` | 반환 개수 (1~50) | `--limit 5` |
124
+ | `--include-deprecated` | 구형 유행어 포함 | `--include-deprecated` |
125
+ | `--index-path` | 외부 인덱스 JSON 경로 | `--index-path ./my-index.json` |
126
+ | `--format` | `json` 또는 `text` | `--format text` |
127
+
128
+ ### 3. (선택) 나무위키 원문 요약 확인
129
+
130
+ ```bash
131
+ python3 scripts/slang_lookup.py "중꺾마" --format json
132
+ python3 scripts/slang_lookup.py "https://namu.wiki/w/%EC%A4%91%EA%BA%BE%EB%A7%88" --max-length 800
133
+ ```
134
+
135
+ - 성공 시 `title`, `summary` 를 돌려준다.
136
+ - 실패 시 `fetched: false` + `block_reason` + `error` 를 돌려준다. 추측하지 말 것.
137
+
138
+ ### 4. 에이전트가 글 작성
139
+
140
+ 1. 후보 중 문맥에 맞는 1~3개를 선택한다. 많이 넣지 않는다.
141
+ 2. `safety` 가 `spicy` 인 표현은 비격식 컨텍스트에서만 쓰고, 사용 의도를 분명히 한다.
142
+ 3. 결과물을 1~2문장으로 자연스럽게 녹인다.
143
+ 4. 필요하면 "사용한 표현: A, B — 의미 확인: [url]" 을 덧붙여 근거 링크를 보여준다.
144
+ 5. 사용자가 더 과한/덜 과한 버전을 원하면 대안을 함께 제공한다.
145
+
146
+ ## CLI examples
147
+
148
+ ```bash
149
+ # 1) 특정 유행어 뜻/메타 바로 확인
150
+ python3 scripts/slang_search.py --query "갓생" --format text
151
+
152
+ # 2) 긍정 무드의 SNS 마케팅 후보 5개
153
+ python3 scripts/slang_search.py --mood "긍정,유머" --context "SNS,마케팅" --safety safe --limit 5
154
+
155
+ # 3) 이용자가 지정한 커스텀 인덱스로 전환
156
+ python3 scripts/slang_search.py --query "중꺾마" --index-path ~/Downloads/my-slang.json
157
+
158
+ # 4) 나무위키 원문 요약 시도
159
+ python3 scripts/slang_lookup.py "럭키비키" --timeout 10 --format text
160
+ ```
161
+
162
+ ## Response policy
163
+
164
+ - 응답에는 **선택한 유행어, 최종 문장, 근거 링크** 세 가지를 포함한다.
165
+ - 유행어를 2개 이상 썼다면 각각 왜 썼는지 한 줄 이유를 붙인다.
166
+ - 사용자가 명시적으로 요청하지 않은 상황에서는 유행어를 끼얹지 않는다.
167
+ - 나무위키 요약이 없을 때는 시드의 `meaning_short` 와 원문 링크로 대체한다. "잘 모르겠다" 를 숨기지 않는다.
168
+
169
+ ## Done when
170
+
171
+ - `slang_search` 가 JSON 으로 최소 한 개의 후보를 돌려주었거나, 의도적으로 0건 (no match) 을 명확히 전달했다.
172
+ - 유행어를 사용한 경우, 각 표현의 뜻/근거 링크가 함께 제시되었다.
173
+ - `safety=risky` 표현은 시드에 없고, `spicy` 표현을 썼다면 왜 괜찮은 문맥인지 한 줄 설명이 있다.
174
+ - 나무위키 lookup 실패 시 추측 대신 원문 링크로 대체되었다.
175
+
176
+ ## Notes
177
+
178
+ - 시드 인덱스 위치: `data/seed-slang.json` (약 30개 항목). 확장은 PR 로 검토 후 반영한다.
179
+ - `last_reviewed` 필드를 의미 있게 업데이트하지 않는 변경은 받지 않는 편이 안전하다.
180
+ - 이 스킬은 **나무위키 분류:유행어** 를 기반으로 하지만 전체 인덱싱하지 않는다. Cloudflare 차단·데이터 선도 변화에 보수적으로 설계된 v1 이다.
181
+ - 다음 버전에서는 여러 공개 소스를 통합하거나 `last_reviewed` 기반 자동 만료 로직을 넣을 수 있다.
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import ssl
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+
8
+
9
+ NAMUWIKI_BASE = "https://namu.wiki/w/"
10
+ _NAMUWIKI_HOSTS = ("namu.wiki", "en.namu.wiki")
11
+
12
+ _BROWSER_HEADERS = {
13
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
14
+ "Accept-Language": "ko,en-US;q=0.9,en;q=0.8",
15
+ "User-Agent": (
16
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
17
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
18
+ ),
19
+ }
20
+
21
+ _ssl_ctx: ssl.SSLContext | None = None
22
+
23
+
24
+ class LookupError(Exception):
25
+ pass
26
+
27
+
28
+ class BlockedError(LookupError):
29
+ pass
30
+
31
+
32
+ class NotFoundError(LookupError):
33
+ pass
34
+
35
+
36
+ class UpstreamError(LookupError):
37
+ pass
38
+
39
+
40
+ def _get_ssl_context() -> ssl.SSLContext:
41
+ global _ssl_ctx
42
+ if _ssl_ctx is None:
43
+ _ssl_ctx = ssl.create_default_context()
44
+ return _ssl_ctx
45
+
46
+
47
+ def is_namuwiki_url(url: str) -> bool:
48
+ parsed = urllib.parse.urlparse(url)
49
+ if parsed.scheme not in {"http", "https"}:
50
+ return False
51
+ host = (parsed.hostname or "").lower()
52
+ return host in _NAMUWIKI_HOSTS
53
+
54
+
55
+ def build_namuwiki_url(term_or_url: str) -> str:
56
+ value = term_or_url.strip()
57
+ if not value:
58
+ raise ValueError("term is empty")
59
+ if is_namuwiki_url(value):
60
+ return value
61
+ # Preserve slash so namuwiki subpage titles (e.g. "한국/서울") survive encoding.
62
+ quoted = urllib.parse.quote(value, safe="/")
63
+ return f"{NAMUWIKI_BASE}{quoted}"
64
+
65
+
66
+ def browser_headers() -> dict[str, str]:
67
+ return dict(_BROWSER_HEADERS)
68
+
69
+
70
+ def fetch_html(url: str, timeout: int) -> str:
71
+ if not is_namuwiki_url(url):
72
+ raise ValueError(f"not a namuwiki URL: {url}")
73
+ request = urllib.request.Request(url, headers=browser_headers())
74
+ try:
75
+ with urllib.request.urlopen(request, timeout=timeout, context=_get_ssl_context()) as response:
76
+ body = response.read()
77
+ except urllib.error.HTTPError as error:
78
+ status = error.code
79
+ if status == 404:
80
+ raise NotFoundError(f"HTTP 404: {url}") from error
81
+ if status in (401, 403, 429):
82
+ raise BlockedError(
83
+ f"HTTP {status} (possibly Cloudflare / rate-limited) for {url}"
84
+ ) from error
85
+ raise UpstreamError(f"HTTP {status} for {url}") from error
86
+ except urllib.error.URLError as error:
87
+ raise UpstreamError(f"URL error for {url}: {error.reason}") from error
88
+ except TimeoutError as error:
89
+ raise UpstreamError(f"timeout after {timeout}s for {url}") from error
90
+
91
+ return body.decode("utf-8", "ignore")