@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.
- package/bin/k-skill.js +60 -0
- package/package.json +37 -0
- package/skills/assembly-bill-vote-search/instruction.md +111 -0
- package/skills/assembly-bill-vote-search/skill.json +10 -0
- package/skills/biz-health-check/instruction.md +69 -0
- package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
- package/skills/biz-health-check/skill.json +10 -0
- package/skills/bok-ecos-stats/instruction.md +95 -0
- package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
- package/skills/bok-ecos-stats/skill.json +10 -0
- package/skills/building-register-search/instruction.md +69 -0
- package/skills/building-register-search/scripts/building_register.py +309 -0
- package/skills/building-register-search/scripts/building_register_xml.py +32 -0
- package/skills/building-register-search/skill.json +9 -0
- package/skills/bunjang-search/instruction.md +154 -0
- package/skills/bunjang-search/skill.json +10 -0
- package/skills/catchtable-sniper/instruction.md +270 -0
- package/skills/catchtable-sniper/skill.json +9 -0
- package/skills/cheap-gas-nearby/instruction.md +107 -0
- package/skills/cheap-gas-nearby/skill.json +10 -0
- package/skills/corporate-registration-consulting/instruction.md +117 -0
- package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
- package/skills/corporate-registration-consulting/skill.json +8 -0
- package/skills/coupang-product-search/instruction.md +219 -0
- package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
- package/skills/coupang-product-search/skill.json +10 -0
- package/skills/court-auction-notice-search/instruction.md +201 -0
- package/skills/court-auction-notice-search/skill.json +9 -0
- package/skills/court-payment-order-assistant/instruction.md +120 -0
- package/skills/court-payment-order-assistant/skill.json +10 -0
- package/skills/d2b-notice-search/instruction.md +117 -0
- package/skills/d2b-notice-search/skill.json +10 -0
- package/skills/daangn-cars-search/instruction.md +91 -0
- package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
- package/skills/daangn-cars-search/skill.json +9 -0
- package/skills/daangn-jobs-search/instruction.md +90 -0
- package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
- package/skills/daangn-jobs-search/skill.json +9 -0
- package/skills/daangn-realty-search/instruction.md +114 -0
- package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
- package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
- package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
- package/skills/daangn-realty-search/skill.json +8 -0
- package/skills/daangn-used-goods-search/instruction.md +90 -0
- package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
- package/skills/daangn-used-goods-search/skill.json +9 -0
- package/skills/daishin-report-search/instruction.md +138 -0
- package/skills/daishin-report-search/skill.json +10 -0
- package/skills/daiso-product-search/instruction.md +172 -0
- package/skills/daiso-product-search/skill.json +8 -0
- package/skills/danawa-price-search/instruction.md +183 -0
- package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
- package/skills/danawa-price-search/skill.json +8 -0
- package/skills/delivery-tracking/instruction.md +358 -0
- package/skills/delivery-tracking/skill.json +8 -0
- package/skills/donation-place-search/instruction.md +129 -0
- package/skills/donation-place-search/skill.json +9 -0
- package/skills/emergency-room-beds/instruction.md +82 -0
- package/skills/emergency-room-beds/skill.json +8 -0
- package/skills/ev-charger-nearby/instruction.md +80 -0
- package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
- package/skills/ev-charger-nearby/skill.json +10 -0
- package/skills/ev-subsidy-status/instruction.md +165 -0
- package/skills/ev-subsidy-status/skill.json +10 -0
- package/skills/express-bus-booking/instruction.md +207 -0
- package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
- package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
- package/skills/express-bus-booking/skill.json +8 -0
- package/skills/fine-dust-location/instruction.md +89 -0
- package/skills/fine-dust-location/skill.json +9 -0
- package/skills/flight-ticket-search/instruction.md +237 -0
- package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
- package/skills/flight-ticket-search/skill.json +9 -0
- package/skills/foresttrip-vacancy/instruction.md +167 -0
- package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
- package/skills/foresttrip-vacancy/skill.json +10 -0
- package/skills/fsc-corporate-info/instruction.md +57 -0
- package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
- package/skills/fsc-corporate-info/skill.json +10 -0
- package/skills/g2b-order-plan-search/instruction.md +131 -0
- package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
- package/skills/g2b-order-plan-search/skill.json +10 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
- package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
- package/skills/g2b-sanctioned-supplier/skill.json +10 -0
- package/skills/gangnamunni-clinic-search/instruction.md +113 -0
- package/skills/gangnamunni-clinic-search/skill.json +9 -0
- package/skills/geeknews-search/instruction.md +69 -0
- package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
- package/skills/geeknews-search/skill.json +8 -0
- package/skills/gongsijiga-search/instruction.md +128 -0
- package/skills/gongsijiga-search/skill.json +9 -0
- package/skills/gov-overseas-trip-report/instruction.md +488 -0
- package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
- package/skills/gov-overseas-trip-report/skill.json +9 -0
- package/skills/han-river-water-level/instruction.md +83 -0
- package/skills/han-river-water-level/skill.json +10 -0
- package/skills/highway-traffic-status/instruction.md +88 -0
- package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
- package/skills/highway-traffic-status/skill.json +11 -0
- package/skills/hipass-receipt/instruction.md +97 -0
- package/skills/hipass-receipt/skill.json +10 -0
- package/skills/hola-poke-yeoksam/instruction.md +247 -0
- package/skills/hola-poke-yeoksam/skill.json +8 -0
- package/skills/household-waste-info/instruction.md +117 -0
- package/skills/household-waste-info/skill.json +10 -0
- package/skills/housing-official-price/instruction.md +177 -0
- package/skills/housing-official-price/skill.json +10 -0
- package/skills/hwp/instruction.md +206 -0
- package/skills/hwp/skill.json +8 -0
- package/skills/intercity-bus-booking/instruction.md +189 -0
- package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
- package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
- package/skills/intercity-bus-booking/skill.json +8 -0
- package/skills/iros-registry-automation/instruction.md +229 -0
- package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
- package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
- package/skills/iros-registry-automation/skill.json +9 -0
- package/skills/job-posting-match/instruction.md +130 -0
- package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
- package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
- package/skills/job-posting-match/skill.json +8 -0
- package/skills/jobkorea-talent-search/instruction.md +118 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
- package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
- package/skills/jobkorea-talent-search/skill.json +9 -0
- package/skills/joseon-sillok-search/instruction.md +76 -0
- package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
- package/skills/joseon-sillok-search/skill.json +8 -0
- package/skills/k-dart/instruction.md +406 -0
- package/skills/k-dart/skill.json +8 -0
- package/skills/k-schoollunch-menu/instruction.md +109 -0
- package/skills/k-schoollunch-menu/skill.json +9 -0
- package/skills/k-skill-cleaner/instruction.md +80 -0
- package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
- package/skills/k-skill-cleaner/skill.json +8 -0
- package/skills/k-skill-setup/instruction.md +253 -0
- package/skills/k-skill-setup/skill.json +11 -0
- package/skills/kakao-bar-nearby/instruction.md +76 -0
- package/skills/kakao-bar-nearby/skill.json +8 -0
- package/skills/kakao-map/instruction.md +176 -0
- package/skills/kakao-map/skill.json +10 -0
- package/skills/kakaotalk-mac/instruction.md +189 -0
- package/skills/kakaotalk-mac/skill.json +8 -0
- package/skills/kbl-results/instruction.md +89 -0
- package/skills/kbl-results/skill.json +9 -0
- package/skills/kbo-results/instruction.md +82 -0
- package/skills/kbo-results/skill.json +8 -0
- package/skills/keris-academic-search/instruction.md +81 -0
- package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
- package/skills/keris-academic-search/skill.json +10 -0
- package/skills/kleague-results/instruction.md +92 -0
- package/skills/kleague-results/skill.json +8 -0
- package/skills/kopis-performance-search/instruction.md +109 -0
- package/skills/kopis-performance-search/skill.json +10 -0
- package/skills/korea-weather/instruction.md +93 -0
- package/skills/korea-weather/skill.json +10 -0
- package/skills/korean-character-count/instruction.md +87 -0
- package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
- package/skills/korean-character-count/skill.json +8 -0
- package/skills/korean-cinema-search/instruction.md +177 -0
- package/skills/korean-cinema-search/skill.json +8 -0
- package/skills/korean-heritage-search/instruction.md +106 -0
- package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
- package/skills/korean-heritage-search/skill.json +8 -0
- package/skills/korean-holiday-calendar/instruction.md +97 -0
- package/skills/korean-holiday-calendar/skill.json +9 -0
- package/skills/korean-humanizer/instruction.md +389 -0
- package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
- package/skills/korean-humanizer/skill.json +8 -0
- package/skills/korean-jangbu-for/instruction.md +133 -0
- package/skills/korean-jangbu-for/scripts/install.sh +237 -0
- package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
- package/skills/korean-jangbu-for/skill.json +8 -0
- package/skills/korean-law-search/instruction.md +126 -0
- package/skills/korean-law-search/skill.json +10 -0
- package/skills/korean-marathon-schedule/instruction.md +111 -0
- package/skills/korean-marathon-schedule/skill.json +9 -0
- package/skills/korean-middle-korean/instruction.md +79 -0
- package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
- package/skills/korean-middle-korean/skill.json +8 -0
- package/skills/korean-patent-search/instruction.md +79 -0
- package/skills/korean-patent-search/scripts/patent_search.py +409 -0
- package/skills/korean-patent-search/skill.json +9 -0
- package/skills/korean-privacy-terms/instruction.md +128 -0
- package/skills/korean-privacy-terms/scripts/install.sh +108 -0
- package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
- package/skills/korean-privacy-terms/skill.json +8 -0
- package/skills/korean-scholarship-search/instruction.md +317 -0
- package/skills/korean-scholarship-search/references/report-format.md +40 -0
- package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
- package/skills/korean-scholarship-search/references/search-clues.md +58 -0
- package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
- package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
- package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
- package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
- package/skills/korean-scholarship-search/skill.json +8 -0
- package/skills/korean-slang-writing/instruction.md +181 -0
- package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
- package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
- package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
- package/skills/korean-slang-writing/skill.json +8 -0
- package/skills/korean-spell-check/instruction.md +105 -0
- package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
- package/skills/korean-spell-check/skill.json +9 -0
- package/skills/korean-stock-search/instruction.md +194 -0
- package/skills/korean-stock-search/skill.json +10 -0
- package/skills/korean-transit-route/instruction.md +113 -0
- package/skills/korean-transit-route/skill.json +10 -0
- package/skills/kosis-stats/instruction.md +232 -0
- package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
- package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
- package/skills/kosis-stats/skill.json +9 -0
- package/skills/kr-whois-lookup/instruction.md +107 -0
- package/skills/kr-whois-lookup/skill.json +10 -0
- package/skills/kstartup-search/instruction.md +186 -0
- package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
- package/skills/kstartup-search/skill.json +11 -0
- package/skills/ktx-booking/instruction.md +244 -0
- package/skills/ktx-booking/skill.json +10 -0
- package/skills/lck-analytics/instruction.md +192 -0
- package/skills/lck-analytics/scripts/_lib.js +103 -0
- package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
- package/skills/lck-analytics/scripts/build-match-report.js +44 -0
- package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
- package/skills/lck-analytics/skill.json +9 -0
- package/skills/lh-notice-search/instruction.md +206 -0
- package/skills/lh-notice-search/skill.json +10 -0
- package/skills/library-book-search/instruction.md +139 -0
- package/skills/library-book-search/skill.json +9 -0
- package/skills/local-election-candidate-search/instruction.md +77 -0
- package/skills/local-election-candidate-search/skill.json +8 -0
- package/skills/localdata-business-status/instruction.md +64 -0
- package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
- package/skills/localdata-business-status/skill.json +8 -0
- package/skills/lotto-results/instruction.md +80 -0
- package/skills/lotto-results/skill.json +8 -0
- package/skills/lovebug-report/instruction.md +185 -0
- package/skills/lovebug-report/skill.json +9 -0
- package/skills/market-kurly-search/instruction.md +125 -0
- package/skills/market-kurly-search/skill.json +8 -0
- package/skills/mfds-drug-safety/instruction.md +86 -0
- package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
- package/skills/mfds-drug-safety/skill.json +10 -0
- package/skills/mfds-food-safety/instruction.md +126 -0
- package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
- package/skills/mfds-food-safety/skill.json +10 -0
- package/skills/myrealtrip-search/instruction.md +239 -0
- package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
- package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
- package/skills/myrealtrip-search/skill.json +9 -0
- package/skills/naming-house/instruction.md +146 -0
- package/skills/naming-house/skill.json +8 -0
- package/skills/national-pension-workplace/instruction.md +64 -0
- package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
- package/skills/national-pension-workplace/skill.json +10 -0
- package/skills/naver-ad-performance/instruction.md +108 -0
- package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
- package/skills/naver-ad-performance/skill.json +9 -0
- package/skills/naver-blog-research/instruction.md +128 -0
- package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
- package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
- package/skills/naver-blog-research/scripts/naver_read.py +256 -0
- package/skills/naver-blog-research/scripts/naver_search.py +192 -0
- package/skills/naver-blog-research/skill.json +8 -0
- package/skills/naver-news-search/instruction.md +103 -0
- package/skills/naver-news-search/skill.json +10 -0
- package/skills/naver-shopping-search/instruction.md +94 -0
- package/skills/naver-shopping-search/skill.json +11 -0
- package/skills/nhis-care-checkup-search/instruction.md +116 -0
- package/skills/nhis-care-checkup-search/skill.json +10 -0
- package/skills/nts-business-registration/instruction.md +115 -0
- package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
- package/skills/nts-business-registration/skill.json +10 -0
- package/skills/nts-tax-delinquency/instruction.md +54 -0
- package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
- package/skills/nts-tax-delinquency/skill.json +8 -0
- package/skills/ohou-today-deal/instruction.md +182 -0
- package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
- package/skills/ohou-today-deal/skill.json +9 -0
- package/skills/olive-young-search/instruction.md +154 -0
- package/skills/olive-young-search/skill.json +8 -0
- package/skills/parking-lot-search/instruction.md +96 -0
- package/skills/parking-lot-search/skill.json +9 -0
- package/skills/popbill/instruction.md +146 -0
- package/skills/popbill/scripts/popbill_cli.py +284 -0
- package/skills/popbill/scripts/popbill_registry.py +54 -0
- package/skills/popbill/scripts/popbill_safety.py +37 -0
- package/skills/popbill/scripts/popbill_templates.py +71 -0
- package/skills/popbill/skill.json +9 -0
- package/skills/public-restroom-nearby/instruction.md +89 -0
- package/skills/public-restroom-nearby/skill.json +9 -0
- package/skills/real-estate-search/instruction.md +172 -0
- package/skills/real-estate-search/skill.json +10 -0
- package/skills/rhwp-advanced/instruction.md +145 -0
- package/skills/rhwp-advanced/skill.json +8 -0
- package/skills/rhwp-edit/instruction.md +153 -0
- package/skills/rhwp-edit/skill.json +8 -0
- package/skills/s2b-notice-search/instruction.md +66 -0
- package/skills/s2b-notice-search/skill.json +9 -0
- package/skills/saju-fortune/instruction.md +177 -0
- package/skills/saju-fortune/skill.json +8 -0
- package/skills/saramin-talent-search/instruction.md +119 -0
- package/skills/saramin-talent-search/skill.json +9 -0
- package/skills/seoul-bike/instruction.md +83 -0
- package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
- package/skills/seoul-bike/skill.json +10 -0
- package/skills/seoul-density/instruction.md +109 -0
- package/skills/seoul-density/scripts/seoul_density.py +271 -0
- package/skills/seoul-density/skill.json +10 -0
- package/skills/seoul-subway-arrival/instruction.md +85 -0
- package/skills/seoul-subway-arrival/skill.json +9 -0
- package/skills/sh-notice-search/instruction.md +150 -0
- package/skills/sh-notice-search/skill.json +9 -0
- package/skills/srt-booking/instruction.md +181 -0
- package/skills/srt-booking/scripts/srt_booking.py +272 -0
- package/skills/srt-booking/scripts/srt_seats.py +156 -0
- package/skills/srt-booking/skill.json +20 -0
- package/skills/subway-lost-property/instruction.md +93 -0
- package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
- package/skills/subway-lost-property/skill.json +8 -0
- package/skills/ticket-availability/instruction.md +175 -0
- package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
- package/skills/ticket-availability/skill.json +9 -0
- package/skills/toss-securities/instruction.md +116 -0
- package/skills/toss-securities/skill.json +9 -0
- package/skills/used-car-price-search/instruction.md +109 -0
- package/skills/used-car-price-search/skill.json +8 -0
- package/skills/yebigun-training/instruction.md +177 -0
- package/skills/yebigun-training/skill.json +9 -0
- package/skills/zipcode-search/instruction.md +159 -0
- package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
- package/skills/zipcode-search/skill.json +8 -0
- package/src/assemble.js +134 -0
- package/src/detect.js +18 -0
- package/templates/action-account.md +4 -0
- package/templates/action-booking.md +5 -0
- package/templates/action-commerce.md +5 -0
- package/templates/action-communication.md +5 -0
- package/templates/action-submission.md +5 -0
- package/templates/browser.md +4 -0
- package/templates/core.md +5 -0
- package/templates/hard-boundary.md +3 -0
- package/templates/local.md +2 -0
- package/templates/lookup.md +2 -0
- package/templates/proxy.md +2 -0
- package/templates/vault.md +4 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from html import unescape
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
from _slang_http import ( # noqa: E402
|
|
14
|
+
BlockedError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
UpstreamError,
|
|
17
|
+
build_namuwiki_url,
|
|
18
|
+
fetch_html,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DEFAULT_TIMEOUT = 15
|
|
23
|
+
DEFAULT_MAX_LENGTH = 1500
|
|
24
|
+
|
|
25
|
+
TAG_RE = re.compile(r"<[^>]+>")
|
|
26
|
+
SCRIPT_STYLE_RE = re.compile(
|
|
27
|
+
r"<(script|style|noscript)[^>]*>.*?</\1>", re.DOTALL | re.IGNORECASE
|
|
28
|
+
)
|
|
29
|
+
TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.DOTALL | re.IGNORECASE)
|
|
30
|
+
NAMUWIKI_TITLE_SUFFIX_RE = re.compile(r"\s*[-|]?\s*나무위키\s*$")
|
|
31
|
+
BLOCK_END_RE = re.compile(r"</(p|div|li|h[1-6])>", re.IGNORECASE)
|
|
32
|
+
BR_RE = re.compile(r"<br\s*/?>", re.IGNORECASE)
|
|
33
|
+
WHITESPACE_RE = re.compile(r"[ \t]+")
|
|
34
|
+
BLANK_LINES_RE = re.compile(r"\n{3,}")
|
|
35
|
+
H2_TAG_RE = re.compile(r"<h2\b[^>]*>.*?</h2>", re.DOTALL | re.IGNORECASE)
|
|
36
|
+
NUMBERED_H2_INNER_TEXT_RE = re.compile(r"^\s*\d+(?:\.\d+)*\.\s+\S")
|
|
37
|
+
SECTION_NUMBER_PREFIX_RE = re.compile(r"^\s*\d+(?:\.\d+)*\.\s+", re.MULTILINE)
|
|
38
|
+
EDIT_AFFORDANCE_RE = re.compile(r"\[\s*편집\s*\]")
|
|
39
|
+
CATEGORY_NAV_RE = re.compile(r"\[\s*펼치기\s*[·・•]\s*접기\s*\][^\n]*")
|
|
40
|
+
DETAILS_PELCHIGI_RE = re.compile(
|
|
41
|
+
r"<details\b[^>]*>"
|
|
42
|
+
r"\s*<summary\b[^>]*>[^<]*펼치기[^<]*</summary>"
|
|
43
|
+
r".*?"
|
|
44
|
+
r"</details>",
|
|
45
|
+
re.DOTALL | re.IGNORECASE,
|
|
46
|
+
)
|
|
47
|
+
OG_DESCRIPTION_RE = re.compile(
|
|
48
|
+
r'<meta\s+[^>]*property\s*=\s*"og:description"\s+[^>]*content\s*=\s*"([^"]*)"',
|
|
49
|
+
re.IGNORECASE,
|
|
50
|
+
)
|
|
51
|
+
OG_DESCRIPTION_REVERSED_RE = re.compile(
|
|
52
|
+
r'<meta\s+[^>]*content\s*=\s*"([^"]*)"\s+[^>]*property\s*=\s*"og:description"',
|
|
53
|
+
re.IGNORECASE,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
MAIN_CONTENT_CLASSES = (
|
|
57
|
+
"wiki-paragraph",
|
|
58
|
+
"wiki-content",
|
|
59
|
+
"namu-wiki-content",
|
|
60
|
+
"article-content",
|
|
61
|
+
"wiki-body",
|
|
62
|
+
"wiki-heading-content",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def fetch_page(url: str, timeout: int) -> str:
|
|
67
|
+
return fetch_html(url, timeout=timeout)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_title(html: str) -> str:
|
|
71
|
+
match = TITLE_RE.search(html)
|
|
72
|
+
if not match:
|
|
73
|
+
return ""
|
|
74
|
+
title = unescape(TAG_RE.sub("", match.group(1))).strip()
|
|
75
|
+
title = NAMUWIKI_TITLE_SUFFIX_RE.sub("", title).strip()
|
|
76
|
+
return title
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _find_main_content(cleaned_html: str) -> str:
|
|
80
|
+
for class_name in MAIN_CONTENT_CLASSES:
|
|
81
|
+
pattern = re.compile(
|
|
82
|
+
rf'<[a-zA-Z]+[^>]*class="[^"]*\b{re.escape(class_name)}\b[^"]*"[^>]*>',
|
|
83
|
+
re.IGNORECASE,
|
|
84
|
+
)
|
|
85
|
+
match = pattern.search(cleaned_html)
|
|
86
|
+
if match:
|
|
87
|
+
return cleaned_html[match.start():]
|
|
88
|
+
|
|
89
|
+
article_match = re.search(r"<article[^>]*>", cleaned_html, re.IGNORECASE)
|
|
90
|
+
if article_match:
|
|
91
|
+
return cleaned_html[article_match.start():]
|
|
92
|
+
|
|
93
|
+
return ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _h2_inner_text(h2_tag_html: str) -> str:
|
|
97
|
+
opening_end = h2_tag_html.index(">") + 1
|
|
98
|
+
closing_start = h2_tag_html.rindex("<")
|
|
99
|
+
inner = h2_tag_html[opening_end:closing_start]
|
|
100
|
+
return unescape(TAG_RE.sub("", inner)).strip()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_numbered_section_h2(h2_tag_html: str) -> bool:
|
|
104
|
+
return bool(NUMBERED_H2_INNER_TEXT_RE.match(_h2_inner_text(h2_tag_html)))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _extract_first_section_between_h2(cleaned_html: str) -> str:
|
|
108
|
+
all_matches = list(H2_TAG_RE.finditer(cleaned_html))
|
|
109
|
+
numbered = [m for m in all_matches if _is_numbered_section_h2(m.group(0))]
|
|
110
|
+
if not numbered:
|
|
111
|
+
return ""
|
|
112
|
+
start = numbered[0].end()
|
|
113
|
+
end = numbered[1].start() if len(numbered) > 1 else len(cleaned_html)
|
|
114
|
+
return cleaned_html[start:end]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _extract_og_description(html: str) -> str:
|
|
118
|
+
match = OG_DESCRIPTION_RE.search(html) or OG_DESCRIPTION_REVERSED_RE.search(html)
|
|
119
|
+
if not match:
|
|
120
|
+
return ""
|
|
121
|
+
return unescape(match.group(1)).strip()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _html_fragment_to_text(fragment: str) -> str:
|
|
125
|
+
text = BR_RE.sub("\n", fragment)
|
|
126
|
+
text = BLOCK_END_RE.sub("\n", text)
|
|
127
|
+
text = TAG_RE.sub("", text)
|
|
128
|
+
text = unescape(text)
|
|
129
|
+
text = EDIT_AFFORDANCE_RE.sub("", text)
|
|
130
|
+
text = CATEGORY_NAV_RE.sub("", text)
|
|
131
|
+
text = SECTION_NUMBER_PREFIX_RE.sub("", text)
|
|
132
|
+
lines: list[str] = []
|
|
133
|
+
for line in text.split("\n"):
|
|
134
|
+
stripped = WHITESPACE_RE.sub(" ", line).strip()
|
|
135
|
+
if stripped:
|
|
136
|
+
lines.append(stripped)
|
|
137
|
+
joined = "\n".join(lines)
|
|
138
|
+
return BLANK_LINES_RE.sub("\n\n", joined).strip()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _truncate(text: str, max_length: int) -> str:
|
|
142
|
+
if max_length > 0 and len(text) > max_length:
|
|
143
|
+
return text[:max_length] + "..."
|
|
144
|
+
return text
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def extract_summary(html: str, *, max_length: int = DEFAULT_MAX_LENGTH) -> str:
|
|
148
|
+
cleaned = SCRIPT_STYLE_RE.sub("", html)
|
|
149
|
+
cleaned = DETAILS_PELCHIGI_RE.sub("", cleaned)
|
|
150
|
+
|
|
151
|
+
h2_section = _extract_first_section_between_h2(cleaned)
|
|
152
|
+
if h2_section:
|
|
153
|
+
text = _html_fragment_to_text(h2_section)
|
|
154
|
+
if text:
|
|
155
|
+
return _truncate(text, max_length)
|
|
156
|
+
|
|
157
|
+
region = _find_main_content(cleaned)
|
|
158
|
+
if region:
|
|
159
|
+
text = _html_fragment_to_text(region)
|
|
160
|
+
if text:
|
|
161
|
+
return _truncate(text, max_length)
|
|
162
|
+
|
|
163
|
+
og_description = _extract_og_description(html)
|
|
164
|
+
if og_description:
|
|
165
|
+
return _truncate(og_description, max_length)
|
|
166
|
+
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _is_url(value: str) -> bool:
|
|
171
|
+
return value.startswith("http://") or value.startswith("https://")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def lookup(
|
|
175
|
+
term_or_url: str,
|
|
176
|
+
*,
|
|
177
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
178
|
+
max_length: int = DEFAULT_MAX_LENGTH,
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
input_value = term_or_url.strip()
|
|
181
|
+
if not input_value:
|
|
182
|
+
raise ValueError("term_or_url is empty")
|
|
183
|
+
|
|
184
|
+
url = build_namuwiki_url(input_value)
|
|
185
|
+
result: dict[str, Any] = {
|
|
186
|
+
"input": term_or_url,
|
|
187
|
+
"url": url,
|
|
188
|
+
"fetched": False,
|
|
189
|
+
"title": "",
|
|
190
|
+
"summary": "",
|
|
191
|
+
"error": None,
|
|
192
|
+
"block_reason": None,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
html = fetch_page(url, timeout=timeout)
|
|
197
|
+
except BlockedError as error:
|
|
198
|
+
result["error"] = str(error)
|
|
199
|
+
result["block_reason"] = "blocked"
|
|
200
|
+
return result
|
|
201
|
+
except NotFoundError as error:
|
|
202
|
+
result["error"] = str(error)
|
|
203
|
+
result["block_reason"] = "not_found"
|
|
204
|
+
return result
|
|
205
|
+
except UpstreamError as error:
|
|
206
|
+
result["error"] = str(error)
|
|
207
|
+
result["block_reason"] = "upstream_error"
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
result["fetched"] = True
|
|
211
|
+
result["title"] = extract_title(html)
|
|
212
|
+
result["summary"] = extract_summary(html, max_length=max_length)
|
|
213
|
+
if not result["summary"]:
|
|
214
|
+
result["warning"] = (
|
|
215
|
+
"Main content region not detected. Namu Wiki HTML layout may have changed; "
|
|
216
|
+
"treat this as a hint and verify meaning from seed index or other sources."
|
|
217
|
+
)
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
222
|
+
parser = argparse.ArgumentParser(
|
|
223
|
+
description=(
|
|
224
|
+
"Fetch a Namu Wiki page for a trending slang term and return a best-effort "
|
|
225
|
+
"summary. Gracefully reports when the upstream blocks the request."
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"term_or_url",
|
|
230
|
+
help="Slang term (e.g. '중꺾마') or full Namu Wiki URL.",
|
|
231
|
+
)
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
"--timeout",
|
|
234
|
+
type=int,
|
|
235
|
+
default=DEFAULT_TIMEOUT,
|
|
236
|
+
help=f"HTTP timeout in seconds. Default: {DEFAULT_TIMEOUT}.",
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
"--max-length",
|
|
240
|
+
type=int,
|
|
241
|
+
default=DEFAULT_MAX_LENGTH,
|
|
242
|
+
help=f"Summary truncation length (0 = unlimited). Default: {DEFAULT_MAX_LENGTH}.",
|
|
243
|
+
)
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
"--format",
|
|
246
|
+
choices=["json", "text"],
|
|
247
|
+
default="json",
|
|
248
|
+
help="Output format.",
|
|
249
|
+
)
|
|
250
|
+
return parser.parse_args(argv)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _format_text(result: dict) -> str:
|
|
254
|
+
lines: list[str] = []
|
|
255
|
+
lines.append(f"URL: {result['url']}")
|
|
256
|
+
if result["fetched"]:
|
|
257
|
+
lines.append(f"Title: {result['title']}")
|
|
258
|
+
lines.append("")
|
|
259
|
+
lines.append(result["summary"] or "(summary not extracted)")
|
|
260
|
+
else:
|
|
261
|
+
lines.append("Fetch failed.")
|
|
262
|
+
lines.append(f"Reason: {result.get('block_reason')}")
|
|
263
|
+
lines.append(f"Detail: {result.get('error')}")
|
|
264
|
+
return "\n".join(lines) + "\n"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main(argv: list[str] | None = None) -> int:
|
|
268
|
+
args = parse_args(argv if argv is not None else sys.argv[1:])
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
result = lookup(
|
|
272
|
+
args.term_or_url,
|
|
273
|
+
timeout=args.timeout,
|
|
274
|
+
max_length=args.max_length,
|
|
275
|
+
)
|
|
276
|
+
except ValueError as error:
|
|
277
|
+
print(
|
|
278
|
+
json.dumps({"error": str(error)}, ensure_ascii=False),
|
|
279
|
+
file=sys.stderr,
|
|
280
|
+
)
|
|
281
|
+
return 1
|
|
282
|
+
|
|
283
|
+
if args.format == "json":
|
|
284
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
285
|
+
else:
|
|
286
|
+
sys.stdout.write(_format_text(result))
|
|
287
|
+
return 0 if result["fetched"] else 2
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any, Iterable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_LIMIT = 10
|
|
13
|
+
MAX_LIMIT = 50
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
|
|
16
|
+
SKILL_ROOT = SCRIPT_DIR.parent
|
|
17
|
+
DEFAULT_INDEX_PATH = SKILL_ROOT / "data" / "seed-slang.json"
|
|
18
|
+
|
|
19
|
+
MATCH_REASON_ORDER = {
|
|
20
|
+
"exact": 0,
|
|
21
|
+
"alias": 1,
|
|
22
|
+
"substring": 2,
|
|
23
|
+
"no-query": 3,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_index(path: str | None = None) -> dict:
|
|
28
|
+
target = pathlib.Path(path) if path else DEFAULT_INDEX_PATH
|
|
29
|
+
if not target.exists():
|
|
30
|
+
raise FileNotFoundError(f"slang index not found at: {target}")
|
|
31
|
+
with target.open(encoding="utf-8") as fh:
|
|
32
|
+
data = json.load(fh)
|
|
33
|
+
if not isinstance(data, dict) or "entries" not in data:
|
|
34
|
+
raise ValueError(f"invalid slang index (missing 'entries'): {target}")
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize(text: str) -> str:
|
|
39
|
+
return " ".join(text.lower().split())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _collect_match(entry: dict, query_norm: str) -> str | None:
|
|
43
|
+
term_norm = _normalize(entry.get("term", ""))
|
|
44
|
+
if not query_norm:
|
|
45
|
+
return "no-query"
|
|
46
|
+
if term_norm == query_norm:
|
|
47
|
+
return "exact"
|
|
48
|
+
aliases = entry.get("aliases") or []
|
|
49
|
+
alias_norms = [_normalize(a) for a in aliases]
|
|
50
|
+
if query_norm in alias_norms:
|
|
51
|
+
return "alias"
|
|
52
|
+
if query_norm in term_norm:
|
|
53
|
+
return "substring"
|
|
54
|
+
for alias_norm in alias_norms:
|
|
55
|
+
if query_norm and query_norm in alias_norm:
|
|
56
|
+
return "substring"
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ensure_list(value: Any) -> list[str]:
|
|
61
|
+
if value is None:
|
|
62
|
+
return []
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
return [value]
|
|
65
|
+
if isinstance(value, Iterable):
|
|
66
|
+
return [str(v) for v in value]
|
|
67
|
+
return [str(value)]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _has_overlap(entry_tags: list[str], requested: list[str]) -> bool:
|
|
71
|
+
if not requested:
|
|
72
|
+
return True
|
|
73
|
+
entry_set = {t.strip().lower() for t in entry_tags}
|
|
74
|
+
requested_set = {t.strip().lower() for t in requested}
|
|
75
|
+
return bool(entry_set & requested_set)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _matches_single(value: str | None, allowed: list[str]) -> bool:
|
|
79
|
+
if not allowed:
|
|
80
|
+
return True
|
|
81
|
+
if value is None:
|
|
82
|
+
return False
|
|
83
|
+
return value.strip().lower() in {a.strip().lower() for a in allowed}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _era_sort_key(era: str) -> int:
|
|
87
|
+
digits = "".join(ch for ch in era if ch.isdigit())
|
|
88
|
+
try:
|
|
89
|
+
return -int(digits[:4]) if digits else 0
|
|
90
|
+
except ValueError:
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def search(
|
|
95
|
+
*,
|
|
96
|
+
query: str | None = None,
|
|
97
|
+
mood: list[str] | None = None,
|
|
98
|
+
context: list[str] | None = None,
|
|
99
|
+
safety: str | list[str] | None = None,
|
|
100
|
+
intensity: str | list[str] | None = None,
|
|
101
|
+
limit: int = DEFAULT_LIMIT,
|
|
102
|
+
include_deprecated: bool = False,
|
|
103
|
+
index: dict | None = None,
|
|
104
|
+
index_path: str | None = None,
|
|
105
|
+
) -> dict:
|
|
106
|
+
if index is None:
|
|
107
|
+
index = load_index(index_path)
|
|
108
|
+
|
|
109
|
+
entries: list[dict] = list(index.get("entries", []))
|
|
110
|
+
|
|
111
|
+
mood_list = _ensure_list(mood)
|
|
112
|
+
context_list = _ensure_list(context)
|
|
113
|
+
safety_list = _ensure_list(safety)
|
|
114
|
+
intensity_list = _ensure_list(intensity)
|
|
115
|
+
|
|
116
|
+
query_norm = _normalize(query) if query else ""
|
|
117
|
+
clamped_limit = max(1, min(int(limit), MAX_LIMIT))
|
|
118
|
+
|
|
119
|
+
scored: list[tuple[int, int, str, dict]] = []
|
|
120
|
+
|
|
121
|
+
for entry in entries:
|
|
122
|
+
if not include_deprecated and not entry.get("still_usable", True):
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
match_reason = _collect_match(entry, query_norm)
|
|
126
|
+
if match_reason is None:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if not _has_overlap(entry.get("mood_tags") or [], mood_list):
|
|
130
|
+
continue
|
|
131
|
+
if not _has_overlap(entry.get("usage_context") or [], context_list):
|
|
132
|
+
continue
|
|
133
|
+
if not _matches_single(entry.get("safety"), safety_list):
|
|
134
|
+
continue
|
|
135
|
+
if not _matches_single(entry.get("intensity"), intensity_list):
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
order = MATCH_REASON_ORDER.get(match_reason, 9)
|
|
139
|
+
era_rank = _era_sort_key(str(entry.get("era", "")))
|
|
140
|
+
scored.append((order, era_rank, str(entry.get("term", "")), {**entry, "match_reason": match_reason}))
|
|
141
|
+
|
|
142
|
+
scored.sort(key=lambda item: (item[0], item[1], item[2]))
|
|
143
|
+
|
|
144
|
+
matched_before_limit = len(scored)
|
|
145
|
+
candidates = [row[3] for row in scored[:clamped_limit]]
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"query": query,
|
|
149
|
+
"filters_applied": {
|
|
150
|
+
"mood": mood_list,
|
|
151
|
+
"context": context_list,
|
|
152
|
+
"safety": safety_list,
|
|
153
|
+
"intensity": intensity_list,
|
|
154
|
+
"limit": clamped_limit,
|
|
155
|
+
"include_deprecated": include_deprecated,
|
|
156
|
+
},
|
|
157
|
+
"matched_before_limit": matched_before_limit,
|
|
158
|
+
"total_candidates": len(candidates),
|
|
159
|
+
"candidates": candidates,
|
|
160
|
+
"source": index.get("source", ""),
|
|
161
|
+
"last_reviewed": index.get("last_reviewed", ""),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _format_text(result: dict) -> str:
|
|
166
|
+
if not result["candidates"]:
|
|
167
|
+
return "No candidates found.\n"
|
|
168
|
+
lines: list[str] = []
|
|
169
|
+
query = result.get("query") or "(no query)"
|
|
170
|
+
lines.append(f"Query: {query}")
|
|
171
|
+
lines.append(
|
|
172
|
+
f"Matched: {result['matched_before_limit']} -> showing {result['total_candidates']}"
|
|
173
|
+
)
|
|
174
|
+
lines.append("")
|
|
175
|
+
for idx, entry in enumerate(result["candidates"], start=1):
|
|
176
|
+
mood = ", ".join(entry.get("mood_tags") or []) or "-"
|
|
177
|
+
context = ", ".join(entry.get("usage_context") or []) or "-"
|
|
178
|
+
lines.append(
|
|
179
|
+
f"{idx}. {entry['term']} ({entry.get('era', '?')}) "
|
|
180
|
+
f"[{entry.get('safety', '?')}, {entry.get('intensity', '?')}]"
|
|
181
|
+
)
|
|
182
|
+
lines.append(f" mood: {mood}")
|
|
183
|
+
lines.append(f" context: {context}")
|
|
184
|
+
lines.append(f" meaning: {entry.get('meaning_short', '')}")
|
|
185
|
+
examples = entry.get("example_usage") or []
|
|
186
|
+
if examples:
|
|
187
|
+
lines.append(f" example: {examples[0]}")
|
|
188
|
+
lines.append(f" match: {entry.get('match_reason', '?')}")
|
|
189
|
+
lines.append(f" url: {entry.get('namuwiki_url', '')}")
|
|
190
|
+
lines.append("")
|
|
191
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _split_csv(value: str | None) -> list[str]:
|
|
195
|
+
if not value:
|
|
196
|
+
return []
|
|
197
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
201
|
+
parser = argparse.ArgumentParser(
|
|
202
|
+
description=(
|
|
203
|
+
"Search curated Korean trending-slang index. "
|
|
204
|
+
"Returns candidates the calling agent can use when writing text with slang."
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
parser.add_argument(
|
|
208
|
+
"--query", default=None, help="Keyword to match against term/aliases."
|
|
209
|
+
)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--mood",
|
|
212
|
+
default="",
|
|
213
|
+
help="Comma-separated mood tags (긍정, 부정, 유머, 의지, ...).",
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"--context",
|
|
217
|
+
default="",
|
|
218
|
+
help="Comma-separated context tags (SNS, 마케팅, 음식, 스포츠, ...).",
|
|
219
|
+
)
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
"--safety",
|
|
222
|
+
default="",
|
|
223
|
+
help="Comma-separated safety levels: safe, spicy, risky.",
|
|
224
|
+
)
|
|
225
|
+
parser.add_argument(
|
|
226
|
+
"--intensity",
|
|
227
|
+
default="",
|
|
228
|
+
help="Comma-separated intensity levels: subtle, medium, strong.",
|
|
229
|
+
)
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"--limit",
|
|
232
|
+
type=int,
|
|
233
|
+
default=DEFAULT_LIMIT,
|
|
234
|
+
help=f"Max candidates to return (1..{MAX_LIMIT}).",
|
|
235
|
+
)
|
|
236
|
+
parser.add_argument(
|
|
237
|
+
"--include-deprecated",
|
|
238
|
+
action="store_true",
|
|
239
|
+
help="Include entries marked still_usable=false.",
|
|
240
|
+
)
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"--index-path",
|
|
243
|
+
default=None,
|
|
244
|
+
help="Override path to a slang index JSON (defaults to bundled seed).",
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument(
|
|
247
|
+
"--format",
|
|
248
|
+
choices=["json", "text"],
|
|
249
|
+
default="json",
|
|
250
|
+
help="Output format. Default: json.",
|
|
251
|
+
)
|
|
252
|
+
return parser.parse_args(argv)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def main(argv: list[str] | None = None) -> int:
|
|
256
|
+
args = parse_args(argv if argv is not None else sys.argv[1:])
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
result = search(
|
|
260
|
+
query=args.query,
|
|
261
|
+
mood=_split_csv(args.mood),
|
|
262
|
+
context=_split_csv(args.context),
|
|
263
|
+
safety=_split_csv(args.safety),
|
|
264
|
+
intensity=_split_csv(args.intensity),
|
|
265
|
+
limit=args.limit,
|
|
266
|
+
include_deprecated=args.include_deprecated,
|
|
267
|
+
index_path=args.index_path,
|
|
268
|
+
)
|
|
269
|
+
except (FileNotFoundError, ValueError) as error:
|
|
270
|
+
print(
|
|
271
|
+
json.dumps({"error": str(error)}, ensure_ascii=False),
|
|
272
|
+
file=sys.stderr,
|
|
273
|
+
)
|
|
274
|
+
return 1
|
|
275
|
+
|
|
276
|
+
if args.format == "json":
|
|
277
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
278
|
+
else:
|
|
279
|
+
sys.stdout.write(_format_text(result))
|
|
280
|
+
return 0
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "korean-slang-writing",
|
|
3
|
+
"description": "Use curated Korean trending-slang candidates plus best-effort Namu Wiki lookups to write witty Korean text with up-to-date slang, with conservative safety and freshness guardrails.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"local"
|
|
6
|
+
],
|
|
7
|
+
"frontmatter": "name: korean-slang-writing\ndescription: Use curated Korean trending-slang candidates plus best-effort Namu Wiki lookups to write witty Korean text with up-to-date slang, with conservative safety and freshness guardrails.\nlicense: MIT\nmetadata:\n category: writing\n locale: ko-KR\n phase: v1"
|
|
8
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Korean Spell Check
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
국립국어원 계열 규칙을 반영한 **바른한글(구 부산대 맞춤법/문법 검사기)** 표면을 이용해 한국어 문장을 최종 교정한다.
|
|
6
|
+
|
|
7
|
+
- 기본 진입점은 공개 웹 표면 `https://nara-speller.co.kr/speller/` 이다.
|
|
8
|
+
- 자동화가 필요하면 이전 버전 폼 POST 표면 `https://nara-speller.co.kr/old_speller/results` 를 **낮은 요청량으로만** 사용한다.
|
|
9
|
+
- 긴 글은 청크로 나눠 순차 검사한다.
|
|
10
|
+
- 결과는 `원문`, `교정안`, `이유` 중심으로 정리한다.
|
|
11
|
+
|
|
12
|
+
## Policy first
|
|
13
|
+
|
|
14
|
+
- `https://nara-speller.co.kr/old_speller/` 는 **비상업적 용도** 안내와 **개인이나 학생만 무료**라는 문구를 명시한다.
|
|
15
|
+
- `https://nara-speller.co.kr/robots.txt` 는 `/` 를 허용하지만 `/test_speller/` 는 금지한다.
|
|
16
|
+
- 따라서 이 스킬은 **사용자 주도 최종 검수**, **저빈도 요청**, **문서/이메일/README 교정** 용도로만 쓴다.
|
|
17
|
+
- 대량 배치, SaaS 백엔드 연동, 상업 서비스 내 무단 재판매/재노출에는 쓰지 않는다. 그런 경우는 공급사 문의/유료 API 계약을 먼저 검토한다.
|
|
18
|
+
|
|
19
|
+
## When to use
|
|
20
|
+
|
|
21
|
+
- "이 한국어 문장 맞춤법 검사해줘"
|
|
22
|
+
- "README 한국어 문장 최종 검수해줘"
|
|
23
|
+
- "마크다운 파일 전체에서 띄어쓰기/맞춤법 오류를 잡아줘"
|
|
24
|
+
- "AI 교정보다 규칙 기반 한국어 검사기로 한 번 더 확인해줘"
|
|
25
|
+
|
|
26
|
+
## When not to use
|
|
27
|
+
|
|
28
|
+
- 코드 블록/로그/영문 위주 텍스트를 그대로 대량 전송해야 하는 경우
|
|
29
|
+
- 민감정보가 많은 원문을 외부 웹 서비스에 보내면 안 되는 경우
|
|
30
|
+
- 상업적 대량 처리 API가 필요한 경우
|
|
31
|
+
|
|
32
|
+
## Prerequisites
|
|
33
|
+
|
|
34
|
+
- 인터넷 연결
|
|
35
|
+
- `python3` 3.10+
|
|
36
|
+
- 이 스킬 디렉토리의 `scripts/korean_spell_check.py` (설치 시 자동 포함)
|
|
37
|
+
|
|
38
|
+
## Verified surface notes
|
|
39
|
+
|
|
40
|
+
- 현재 공개 사이트는 `https://nara-speller.co.kr/speller/` 로 제공된다.
|
|
41
|
+
- 이 환경에서 일반 shell/Node fetch는 Cloudflare 때문에 `403` 이 나올 수 있었다.
|
|
42
|
+
- 같은 환경에서도 **브라우저형 User-Agent + Python stdlib `urllib` POST** 는 `old_speller/results` 에서 실제 검사 결과 HTML을 반환했다.
|
|
43
|
+
- 무료 공개 표면은 HTML 결과 페이지이며, 문서화된 공개 JSON API는 확인하지 못했다.
|
|
44
|
+
|
|
45
|
+
## Workflow
|
|
46
|
+
|
|
47
|
+
### 1. Ask for the text or file path
|
|
48
|
+
|
|
49
|
+
- 텍스트가 직접 주어지면 바로 검사한다.
|
|
50
|
+
- 파일 검사라면 UTF-8 텍스트/Markdown 파일만 대상으로 잡고, 코드 블록이 많으면 먼저 사용자에게 범위를 줄일지 물어보는 편이 안전하다.
|
|
51
|
+
|
|
52
|
+
### 2. Keep requests conservative
|
|
53
|
+
|
|
54
|
+
- 기본 청크 크기는 `1500` 자 안팎으로 유지한다.
|
|
55
|
+
- 청크 사이는 최소 `1초` 정도 쉬게 한다.
|
|
56
|
+
- 한 번에 너무 많은 파일을 돌리지 않는다.
|
|
57
|
+
|
|
58
|
+
### 3. Run the helper
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python3 scripts/korean_spell_check.py \
|
|
62
|
+
--file README.md \
|
|
63
|
+
--format json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
짧은 문장은 `--text` 로 바로 넣을 수 있다.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python3 scripts/korean_spell_check.py \
|
|
70
|
+
--text "아버지가방에들어가신다." \
|
|
71
|
+
--format text
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. Return change-focused output
|
|
75
|
+
|
|
76
|
+
최종 답변은 아래 순서를 권장한다.
|
|
77
|
+
|
|
78
|
+
1. 교정된 전체 문장/문단
|
|
79
|
+
2. 주요 변경점 목록
|
|
80
|
+
3. 각 변경점의 `원문`, `교정안`, `이유`
|
|
81
|
+
4. 필요하면 `공개 웹 검사기 기준 결과이며, 최종 문맥 판단은 사람이 확인` 문구
|
|
82
|
+
|
|
83
|
+
예시 JSON 필드:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"original": "아버지가방에들어가신다",
|
|
88
|
+
"suggestions": ["아버지가 방에 들어가신다"],
|
|
89
|
+
"reason": "띄어쓰기, 붙여쓰기, 음절 대치와 같은 교정 방법에 따라 수정한 결과입니다."
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Done when
|
|
94
|
+
|
|
95
|
+
- 공개 표면 정책을 먼저 확인했다.
|
|
96
|
+
- 긴 텍스트면 청크 분할을 적용했다.
|
|
97
|
+
- 결과를 `원문/교정안/이유` 중심으로 정리했다.
|
|
98
|
+
- 고빈도/상업적 사용이 아님을 분명히 했다.
|
|
99
|
+
|
|
100
|
+
## Notes
|
|
101
|
+
|
|
102
|
+
- guide: `https://nara-speller.co.kr/guide/`
|
|
103
|
+
- main UI: `https://nara-speller.co.kr/speller/`
|
|
104
|
+
- old UI / form post: `https://nara-speller.co.kr/old_speller/`, `https://nara-speller.co.kr/old_speller/results`
|
|
105
|
+
- robots: `https://nara-speller.co.kr/robots.txt`
|