@kortix/sandbox 0.4.1
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/config/customize.sh +143 -0
- package/config/kortix-env-setup.sh +25 -0
- package/kortix-master/package.json +22 -0
- package/kortix-master/src/config.ts +22 -0
- package/kortix-master/src/index.ts +44 -0
- package/kortix-master/src/routes/env.ts +65 -0
- package/kortix-master/src/routes/proxy.ts +108 -0
- package/kortix-master/src/routes/update.ts +185 -0
- package/kortix-master/src/services/proxy.ts +43 -0
- package/kortix-master/src/services/secret-store.ts +156 -0
- package/kortix-master/tsconfig.json +14 -0
- package/opencode/agents/kortix-browser.md +142 -0
- package/opencode/agents/kortix-build.md +62 -0
- package/opencode/agents/kortix-explore.md +66 -0
- package/opencode/agents/kortix-image-gen.md +33 -0
- package/opencode/agents/kortix-main.md +450 -0
- package/opencode/agents/kortix-plan.md +100 -0
- package/opencode/agents/kortix-research.md +84 -0
- package/opencode/agents/kortix-sheets.md +61 -0
- package/opencode/agents/kortix-slides.md +64 -0
- package/opencode/agents/kortix-web-dev.md +572 -0
- package/opencode/commands/email.md +36 -0
- package/opencode/commands/init.md +43 -0
- package/opencode/commands/journal.md +44 -0
- package/opencode/commands/memory-init.md +81 -0
- package/opencode/commands/memory-search.md +50 -0
- package/opencode/commands/memory-status.md +56 -0
- package/opencode/commands/research.md +36 -0
- package/opencode/commands/search.md +38 -0
- package/opencode/commands/slides.md +32 -0
- package/opencode/commands/spreadsheet.md +30 -0
- package/opencode/memory.json +37 -0
- package/opencode/ocx.jsonc +10 -0
- package/opencode/opencode.jsonc +103 -0
- package/opencode/package.json +25 -0
- package/opencode/patches/apply.sh +19 -0
- package/opencode/patches/opencode-pty-spawn.txt +49 -0
- package/opencode/plugin/background-agents.ts.disabled +483 -0
- package/opencode/plugin/kdco-primitives/get-project-id.ts +172 -0
- package/opencode/plugin/kdco-primitives/index.ts +26 -0
- package/opencode/plugin/kdco-primitives/log-warn.ts +51 -0
- package/opencode/plugin/kdco-primitives/mutex.ts +122 -0
- package/opencode/plugin/kdco-primitives/shell.ts +138 -0
- package/opencode/plugin/kdco-primitives/temp.ts +36 -0
- package/opencode/plugin/kdco-primitives/terminal-detect.ts +34 -0
- package/opencode/plugin/kdco-primitives/types.ts +13 -0
- package/opencode/plugin/kdco-primitives/with-timeout.ts +84 -0
- package/opencode/plugin/memory.ts +306 -0
- package/opencode/plugin/worktree/state.ts +412 -0
- package/opencode/plugin/worktree/terminal.ts +1002 -0
- package/opencode/plugin/worktree.ts +861 -0
- package/opencode/skills/KORTIX-browser/SKILL.md +478 -0
- package/opencode/skills/KORTIX-cron-triggers/SKILL.md +173 -0
- package/opencode/skills/KORTIX-deep-research/SKILL.md +278 -0
- package/opencode/skills/KORTIX-docx/SKILL.md +398 -0
- package/opencode/skills/KORTIX-docx/scripts/__init__.py +1 -0
- package/opencode/skills/KORTIX-docx/scripts/accept_changes.py +104 -0
- package/opencode/skills/KORTIX-docx/scripts/comment.py +244 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-docx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-docx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-docx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-docx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-docx/scripts/render_docx.py +179 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/comments.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtended.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsExtensible.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/commentsIds.xml +3 -0
- package/opencode/skills/KORTIX-docx/scripts/templates/people.xml +3 -0
- package/opencode/skills/KORTIX-domain-research/SKILL.md +96 -0
- package/opencode/skills/KORTIX-domain-research/scripts/domain-lookup.py +810 -0
- package/opencode/skills/KORTIX-elevenlabs/SKILL.md +230 -0
- package/opencode/skills/KORTIX-elevenlabs/scripts/tts.py +389 -0
- package/opencode/skills/KORTIX-email/SKILL.md +145 -0
- package/opencode/skills/KORTIX-legal-writer/SKILL.md +409 -0
- package/opencode/skills/KORTIX-legal-writer/references/bluebook.md +152 -0
- package/opencode/skills/KORTIX-legal-writer/references/document-types.md +416 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/courtlistener.py +291 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/ecfr_lookup.py +299 -0
- package/opencode/skills/KORTIX-legal-writer/scripts/verify-legal.py +507 -0
- package/opencode/skills/KORTIX-logo-creator/SKILL.md +293 -0
- package/opencode/skills/KORTIX-logo-creator/references/prompt-patterns.md +134 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/compose_logo.py +406 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/create_logo_sheet.py +258 -0
- package/opencode/skills/KORTIX-logo-creator/scripts/remove_bg.py +96 -0
- package/opencode/skills/KORTIX-memory/SKILL.md +261 -0
- package/opencode/skills/KORTIX-memory/scripts/export-sessions.py +409 -0
- package/opencode/skills/KORTIX-paper-creator/SKILL.md +549 -0
- package/opencode/skills/KORTIX-paper-creator/assets/template.tex +101 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/compile.sh +177 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/openalex_to_bibtex.py +220 -0
- package/opencode/skills/KORTIX-paper-creator/scripts/verify.sh +354 -0
- package/opencode/skills/KORTIX-paper-search/SKILL.md +418 -0
- package/opencode/skills/KORTIX-pdf/SKILL.md +232 -0
- package/opencode/skills/KORTIX-pdf/forms.md +36 -0
- package/opencode/skills/KORTIX-pdf/reference.md +105 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_bounding_boxes.py +65 -0
- package/opencode/skills/KORTIX-pdf/scripts/check_fillable_fields.py +11 -0
- package/opencode/skills/KORTIX-pdf/scripts/convert_pdf_to_images.py +33 -0
- package/opencode/skills/KORTIX-pdf/scripts/create_validation_image.py +37 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_field_info.py +122 -0
- package/opencode/skills/KORTIX-pdf/scripts/extract_form_structure.py +115 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_fillable_fields.py +98 -0
- package/opencode/skills/KORTIX-pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/opencode/skills/KORTIX-plan/SKILL.md +228 -0
- package/opencode/skills/KORTIX-presentation-viewer/SKILL.md +87 -0
- package/opencode/skills/KORTIX-presentation-viewer/serve.ts +136 -0
- package/opencode/skills/KORTIX-presentation-viewer/viewer.html +559 -0
- package/opencode/skills/KORTIX-presentations/SKILL.md +344 -0
- package/opencode/skills/KORTIX-remotion/SKILL.md +56 -0
- package/opencode/skills/KORTIX-remotion/rules/3d.md +86 -0
- package/opencode/skills/KORTIX-remotion/rules/animations.md +29 -0
- package/opencode/skills/KORTIX-remotion/rules/assets.md +78 -0
- package/opencode/skills/KORTIX-remotion/rules/audio-visualization.md +198 -0
- package/opencode/skills/KORTIX-remotion/rules/audio.md +169 -0
- package/opencode/skills/KORTIX-remotion/rules/calculate-metadata.md +104 -0
- package/opencode/skills/KORTIX-remotion/rules/can-decode.md +75 -0
- package/opencode/skills/KORTIX-remotion/rules/charts.md +120 -0
- package/opencode/skills/KORTIX-remotion/rules/compositions.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/display-captions.md +184 -0
- package/opencode/skills/KORTIX-remotion/rules/extract-frames.md +229 -0
- package/opencode/skills/KORTIX-remotion/rules/ffmpeg.md +38 -0
- package/opencode/skills/KORTIX-remotion/rules/fonts.md +152 -0
- package/opencode/skills/KORTIX-remotion/rules/get-audio-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-dimensions.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/get-video-duration.md +58 -0
- package/opencode/skills/KORTIX-remotion/rules/gifs.md +141 -0
- package/opencode/skills/KORTIX-remotion/rules/images.md +130 -0
- package/opencode/skills/KORTIX-remotion/rules/import-srt-captions.md +69 -0
- package/opencode/skills/KORTIX-remotion/rules/light-leaks.md +73 -0
- package/opencode/skills/KORTIX-remotion/rules/lottie.md +68 -0
- package/opencode/skills/KORTIX-remotion/rules/maps.md +401 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-dom-nodes.md +35 -0
- package/opencode/skills/KORTIX-remotion/rules/measuring-text.md +143 -0
- package/opencode/skills/KORTIX-remotion/rules/parameters.md +98 -0
- package/opencode/skills/KORTIX-remotion/rules/sequencing.md +118 -0
- package/opencode/skills/KORTIX-remotion/rules/subtitles.md +36 -0
- package/opencode/skills/KORTIX-remotion/rules/tailwind.md +11 -0
- package/opencode/skills/KORTIX-remotion/rules/text-animations.md +20 -0
- package/opencode/skills/KORTIX-remotion/rules/timing.md +179 -0
- package/opencode/skills/KORTIX-remotion/rules/transcribe-captions.md +70 -0
- package/opencode/skills/KORTIX-remotion/rules/transitions.md +197 -0
- package/opencode/skills/KORTIX-remotion/rules/transparent-videos.md +106 -0
- package/opencode/skills/KORTIX-remotion/rules/trimming.md +53 -0
- package/opencode/skills/KORTIX-remotion/rules/videos.md +171 -0
- package/opencode/skills/KORTIX-secrets/SKILL.md +280 -0
- package/opencode/skills/KORTIX-semantic-search/SKILL.md +213 -0
- package/opencode/skills/KORTIX-session-search/SKILL.md +807 -0
- package/opencode/skills/KORTIX-session-search/Untitled +1 -0
- package/opencode/skills/KORTIX-skill-creator/SKILL.md +163 -0
- package/opencode/skills/KORTIX-web-research/SKILL.md +69 -0
- package/opencode/skills/KORTIX-xlsx/LICENSE.txt +30 -0
- package/opencode/skills/KORTIX-xlsx/SKILL.md +549 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/__init__.py +0 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/pack.py +159 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/soffice.py +183 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/unpack.py +132 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validate.py +111 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/__init__.py +15 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/base.py +847 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/docx.py +446 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/pptx.py +275 -0
- package/opencode/skills/KORTIX-xlsx/scripts/office/validators/redlining.py +247 -0
- package/opencode/skills/KORTIX-xlsx/scripts/recalc.py +184 -0
- package/opencode/tools/image-gen.ts +342 -0
- package/opencode/tools/image-search.ts +190 -0
- package/opencode/tools/memory-get.ts +168 -0
- package/opencode/tools/memory-search.ts +247 -0
- package/opencode/tools/presentation-gen.ts +723 -0
- package/opencode/tools/scrape-webpage.ts +115 -0
- package/opencode/tools/scripts/.python-version +1 -0
- package/opencode/tools/scripts/convert_pdf.py +184 -0
- package/opencode/tools/scripts/convert_pptx.py +562 -0
- package/opencode/tools/scripts/pyproject.toml +11 -0
- package/opencode/tools/scripts/uv.lock +287 -0
- package/opencode/tools/scripts/validate_slide.py +74 -0
- package/opencode/tools/show-user.ts +217 -0
- package/opencode/tools/tests/e2e-presentation-fix.ts +277 -0
- package/opencode/tools/tests/image-gen.test.ts +215 -0
- package/opencode/tools/tests/image-search.test.ts +125 -0
- package/opencode/tools/tests/memory-system-benchmark.ts +1076 -0
- package/opencode/tools/tests/presentation-gen.test.ts +389 -0
- package/opencode/tools/tests/scrape-webpage.test.ts +74 -0
- package/opencode/tools/tests/show-user.test.ts +241 -0
- package/opencode/tools/tests/video-gen.test.ts +110 -0
- package/opencode/tools/tests/web-search.test.ts +106 -0
- package/opencode/tools/video-gen.ts +200 -0
- package/opencode/tools/web-search.ts +153 -0
- package/opencode/tsconfig.json +29 -0
- package/package.json +36 -0
- package/patch-agent-browser.js +100 -0
- package/postinstall.sh +88 -0
- package/services/KORTIX-presentation-viewer/run +37 -0
- package/services/agent-browser-viewer/run +48 -0
- package/services/kortix-master/run +16 -0
- package/services/lss-sync/run +22 -0
- package/services/opencode-serve/run +25 -0
- package/services/opencode-web/run +21 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Domain Research CLI -- Free domain availability checking, WHOIS/RDAP lookup.
|
|
4
|
+
Zero credentials required. Uses RDAP (1195+ TLDs) with whois CLI fallback.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python3 domain-lookup.py <command> [options]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
check <domain1,domain2,...> Check domain availability
|
|
11
|
+
search <keyword> [--tlds .com,.net] Search keyword across TLDs
|
|
12
|
+
whois <domain> Full WHOIS/RDAP lookup
|
|
13
|
+
expiry <domain> Check expiration date
|
|
14
|
+
nameservers <domain> Get nameservers
|
|
15
|
+
bulk <file> Check domains from file (one per line)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
import json
|
|
20
|
+
import argparse
|
|
21
|
+
import subprocess
|
|
22
|
+
import urllib.request
|
|
23
|
+
import urllib.error
|
|
24
|
+
import re
|
|
25
|
+
import os
|
|
26
|
+
import time
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
29
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
30
|
+
|
|
31
|
+
# ─── RDAP BOOTSTRAP ──────────────────────────────────────────
|
|
32
|
+
# Instead of querying rdap.org (which rate-limits aggressively),
|
|
33
|
+
# we download the IANA bootstrap file once and query registry
|
|
34
|
+
# RDAP servers directly. Each registry only serves its own TLDs
|
|
35
|
+
# so rate limits are generous.
|
|
36
|
+
|
|
37
|
+
IANA_BOOTSTRAP_URL = "https://data.iana.org/rdap/dns.json"
|
|
38
|
+
RDAP_FALLBACK = "https://rdap.org/domain/" # Last resort only
|
|
39
|
+
|
|
40
|
+
# Cache bootstrap data in memory (loaded once per run)
|
|
41
|
+
_rdap_bootstrap: Optional[Dict[str, str]] = None # tld -> rdap_base_url
|
|
42
|
+
_bootstrap_loaded = False
|
|
43
|
+
|
|
44
|
+
# Cache file: avoid re-downloading every single run
|
|
45
|
+
_CACHE_DIR = Path.home() / ".cache" / "domain-research"
|
|
46
|
+
_CACHE_FILE = _CACHE_DIR / "rdap-bootstrap.json"
|
|
47
|
+
_CACHE_MAX_AGE = 86400 * 7 # 7 days
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load_bootstrap() -> Dict[str, str]:
|
|
51
|
+
"""Load TLD -> RDAP URL mapping from IANA bootstrap. Cached to disk."""
|
|
52
|
+
global _rdap_bootstrap, _bootstrap_loaded
|
|
53
|
+
if _bootstrap_loaded and _rdap_bootstrap is not None:
|
|
54
|
+
return _rdap_bootstrap
|
|
55
|
+
|
|
56
|
+
# Try disk cache first
|
|
57
|
+
mapping = _load_bootstrap_cache()
|
|
58
|
+
if mapping:
|
|
59
|
+
_rdap_bootstrap = mapping
|
|
60
|
+
_bootstrap_loaded = True
|
|
61
|
+
return mapping
|
|
62
|
+
|
|
63
|
+
# Download fresh
|
|
64
|
+
mapping = _download_bootstrap()
|
|
65
|
+
if mapping:
|
|
66
|
+
_save_bootstrap_cache(mapping)
|
|
67
|
+
_rdap_bootstrap = mapping
|
|
68
|
+
_bootstrap_loaded = True
|
|
69
|
+
return mapping
|
|
70
|
+
|
|
71
|
+
# Empty fallback — will use rdap.org
|
|
72
|
+
_rdap_bootstrap = {}
|
|
73
|
+
_bootstrap_loaded = True
|
|
74
|
+
return _rdap_bootstrap
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load_bootstrap_cache() -> Optional[Dict[str, str]]:
|
|
78
|
+
"""Load cached bootstrap from disk if fresh enough."""
|
|
79
|
+
try:
|
|
80
|
+
if _CACHE_FILE.exists():
|
|
81
|
+
age = time.time() - _CACHE_FILE.stat().st_mtime
|
|
82
|
+
if age < _CACHE_MAX_AGE:
|
|
83
|
+
with open(_CACHE_FILE) as f:
|
|
84
|
+
return json.load(f)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _save_bootstrap_cache(mapping: Dict[str, str]):
|
|
91
|
+
"""Save bootstrap to disk cache."""
|
|
92
|
+
try:
|
|
93
|
+
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
with open(_CACHE_FILE, "w") as f:
|
|
95
|
+
json.dump(mapping, f)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass # Non-critical
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _download_bootstrap() -> Optional[Dict[str, str]]:
|
|
101
|
+
"""Download IANA RDAP bootstrap and build TLD -> URL mapping."""
|
|
102
|
+
try:
|
|
103
|
+
req = urllib.request.Request(IANA_BOOTSTRAP_URL)
|
|
104
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
105
|
+
data = json.loads(resp.read())
|
|
106
|
+
mapping = {}
|
|
107
|
+
for entry in data.get("services", []):
|
|
108
|
+
tlds, urls = entry
|
|
109
|
+
rdap_url = urls[0] if urls else None
|
|
110
|
+
if rdap_url:
|
|
111
|
+
# Ensure trailing slash
|
|
112
|
+
if not rdap_url.endswith("/"):
|
|
113
|
+
rdap_url += "/"
|
|
114
|
+
for tld in tlds:
|
|
115
|
+
mapping[tld.lower()] = rdap_url
|
|
116
|
+
return mapping
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_rdap_url(domain: str) -> str:
|
|
122
|
+
"""Get the best RDAP URL for a domain. Registry-direct when possible."""
|
|
123
|
+
bootstrap = _load_bootstrap()
|
|
124
|
+
tld = domain.rsplit(".", 1)[-1].lower() if "." in domain else ""
|
|
125
|
+
|
|
126
|
+
if tld in bootstrap:
|
|
127
|
+
base = bootstrap[tld]
|
|
128
|
+
return f"{base}domain/{domain}"
|
|
129
|
+
|
|
130
|
+
# Fallback to rdap.org proxy
|
|
131
|
+
return f"{RDAP_FALLBACK}{domain}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ─── WHOIS PATTERNS ──────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
# Patterns indicating domain is available (order matters: more specific first)
|
|
137
|
+
AVAIL_PATTERNS = re.compile(
|
|
138
|
+
r"(?:"
|
|
139
|
+
r"^No match for " # Verisign .com/.net
|
|
140
|
+
r"|^NOT FOUND\b" # Various registries
|
|
141
|
+
r"|^No Data Found" # Some registries
|
|
142
|
+
r"|^The queried object does not exist" # CentralNic (.store, .online, etc.)
|
|
143
|
+
r"|^No entries found" # DENIC .de
|
|
144
|
+
r"|^Domain not found" # Various
|
|
145
|
+
r"|^No such domain" # Various
|
|
146
|
+
r"|^Status:\s*(?:free|available|AVAILABLE)" # Some ccTLD registries
|
|
147
|
+
r"|^%% No matching objects" # RIPE-style
|
|
148
|
+
r"|^This domain name has not been registered" # .hk
|
|
149
|
+
r"|^The domain has not been registered" # .tw
|
|
150
|
+
r"|^Object does not exist" # Various
|
|
151
|
+
r"|DOMAIN NOT FOUND" # Various (case-insensitive handled by flag)
|
|
152
|
+
r"|is free$" # Some registries
|
|
153
|
+
r"|^not registered" # Some registries
|
|
154
|
+
r")",
|
|
155
|
+
re.IGNORECASE | re.MULTILINE,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Patterns indicating domain is taken — these are strong positive signals
|
|
159
|
+
TAKEN_PATTERNS = re.compile(
|
|
160
|
+
r"(?:"
|
|
161
|
+
r"^Domain Name:\s*\S+" # Standard WHOIS domain record
|
|
162
|
+
r"|^Registry Domain ID:\s*\S+" # ICANN registries
|
|
163
|
+
r"|^Registrar:\s*\S+" # Has a registrar = registered
|
|
164
|
+
r"|^Creation Date:\s*\S+" # Has creation date = registered
|
|
165
|
+
r"|^Registry Expiry Date:\s*\S+" # Has expiry = registered
|
|
166
|
+
r"|^Registrar Registration Expiration Date:" # Alternative expiry format
|
|
167
|
+
r"|^created:\s*\S+" # ccTLD format
|
|
168
|
+
r"|^registered:\s*\S+" # .uk format
|
|
169
|
+
r"|^Registration Date:\s*\S+" # Some registries
|
|
170
|
+
r"|^Domain Status:\s*\S+" # ICANN status flags
|
|
171
|
+
r"|^Registered on:\s*\S+" # .uk
|
|
172
|
+
r")",
|
|
173
|
+
re.IGNORECASE | re.MULTILINE,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
DEFAULT_TLDS = [
|
|
178
|
+
".com", ".net", ".org", ".io", ".co", ".ai", ".dev", ".app",
|
|
179
|
+
".xyz", ".me", ".tech", ".cloud", ".sh", ".so", ".gg",
|
|
180
|
+
".info", ".biz", ".us", ".online", ".site", ".store",
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ─── RDAP LOOKUP ─────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def rdap_lookup(domain: str, retries: int = 2) -> Tuple[Optional[Dict], int]:
|
|
187
|
+
"""Query RDAP for a domain via its registry server. Returns (data_or_None, http_status)."""
|
|
188
|
+
url = _get_rdap_url(domain)
|
|
189
|
+
for attempt in range(retries + 1):
|
|
190
|
+
req = urllib.request.Request(url, headers={"Accept": "application/rdap+json,application/json"})
|
|
191
|
+
try:
|
|
192
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
193
|
+
body = resp.read().decode("utf-8")
|
|
194
|
+
return json.loads(body), resp.status
|
|
195
|
+
except urllib.error.HTTPError as e:
|
|
196
|
+
if e.code == 404:
|
|
197
|
+
return None, 404
|
|
198
|
+
if e.code in (429, 502, 503) and attempt < retries:
|
|
199
|
+
time.sleep(1.5 * (attempt + 1))
|
|
200
|
+
continue
|
|
201
|
+
return None, e.code
|
|
202
|
+
except Exception:
|
|
203
|
+
if attempt < retries:
|
|
204
|
+
time.sleep(1.5 * (attempt + 1))
|
|
205
|
+
continue
|
|
206
|
+
return None, 0
|
|
207
|
+
return None, 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ─── WHOIS LOOKUP ────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
def _run_whois(domain: str, server: Optional[str] = None) -> Optional[str]:
|
|
213
|
+
"""Run whois CLI, optionally targeting a specific server."""
|
|
214
|
+
try:
|
|
215
|
+
cmd = ["whois"]
|
|
216
|
+
if server:
|
|
217
|
+
cmd += ["-h", server]
|
|
218
|
+
cmd.append(domain)
|
|
219
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
|
220
|
+
return result.stdout + result.stderr
|
|
221
|
+
except FileNotFoundError:
|
|
222
|
+
return None
|
|
223
|
+
except subprocess.TimeoutExpired:
|
|
224
|
+
return None
|
|
225
|
+
except Exception:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Known registry whois servers for TLDs where the default whois referral chain fails
|
|
230
|
+
_REGISTRY_WHOIS = {
|
|
231
|
+
".me": "whois.nic.me",
|
|
232
|
+
".io": "whois.nic.io",
|
|
233
|
+
".co": "whois.registry.co",
|
|
234
|
+
".sh": "whois.nic.sh",
|
|
235
|
+
".gg": "whois.gg",
|
|
236
|
+
".so": "whois.nic.so",
|
|
237
|
+
".cc": "ccwhois.verisign-grs.com",
|
|
238
|
+
".tv": "tvwhois.verisign-grs.com",
|
|
239
|
+
".us": "whois.nic.us",
|
|
240
|
+
".in": "whois.registry.in",
|
|
241
|
+
".de": "whois.denic.de",
|
|
242
|
+
".uk": "whois.nic.uk",
|
|
243
|
+
".eu": "whois.eu",
|
|
244
|
+
".ca": "whois.cira.ca",
|
|
245
|
+
".au": "whois.auda.org.au",
|
|
246
|
+
".nl": "whois.domain-registry.nl",
|
|
247
|
+
".br": "whois.registro.br",
|
|
248
|
+
".fr": "whois.nic.fr",
|
|
249
|
+
".it": "whois.nic.it",
|
|
250
|
+
".jp": "whois.jprs.jp",
|
|
251
|
+
".ru": "whois.tcinet.ru",
|
|
252
|
+
".cn": "whois.cnnic.cn",
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def whois_lookup(domain: str) -> Optional[str]:
|
|
257
|
+
"""Run whois CLI for a domain. Follows referrals and queries registry directly."""
|
|
258
|
+
tld = "." + domain.rsplit(".", 1)[-1].lower() if "." in domain else ""
|
|
259
|
+
|
|
260
|
+
# Strategy 1: For known problematic TLDs, go straight to the registry
|
|
261
|
+
if tld in _REGISTRY_WHOIS:
|
|
262
|
+
registry_output = _run_whois(domain, _REGISTRY_WHOIS[tld])
|
|
263
|
+
if registry_output and len(registry_output.strip()) > 20:
|
|
264
|
+
return registry_output
|
|
265
|
+
|
|
266
|
+
# Strategy 2: Default whois command (follows IANA referrals on most systems)
|
|
267
|
+
output = _run_whois(domain)
|
|
268
|
+
if output is None:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
# Check if we only got TLD-level info with no domain data
|
|
272
|
+
has_domain_data = bool(re.search(
|
|
273
|
+
r"^(?:Domain Name|Registry Domain ID|Registrar|Creation Date|"
|
|
274
|
+
r"Domain Status|created:|registered:):\s*\S",
|
|
275
|
+
output, re.MULTILINE | re.IGNORECASE
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
if has_domain_data:
|
|
279
|
+
return output
|
|
280
|
+
|
|
281
|
+
# Parse "refer:" from IANA response and follow it
|
|
282
|
+
refer_match = re.search(r"^refer:\s*(\S+)", output, re.MULTILINE | re.IGNORECASE)
|
|
283
|
+
if refer_match:
|
|
284
|
+
registry_output = _run_whois(domain, refer_match.group(1))
|
|
285
|
+
if registry_output and len(registry_output.strip()) > 20:
|
|
286
|
+
return registry_output
|
|
287
|
+
|
|
288
|
+
# Also try "whois:" field (some IANA entries use this instead of "refer:")
|
|
289
|
+
whois_match = re.search(r"^whois:\s*(\S+)", output, re.MULTILINE | re.IGNORECASE)
|
|
290
|
+
if whois_match and whois_match.group(1).strip():
|
|
291
|
+
registry_output = _run_whois(domain, whois_match.group(1))
|
|
292
|
+
if registry_output and len(registry_output.strip()) > 20:
|
|
293
|
+
return registry_output
|
|
294
|
+
|
|
295
|
+
return output
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def check_availability_whois(domain: str) -> str:
|
|
299
|
+
"""Check availability via whois CLI. Returns 'available'|'taken'|'unknown'."""
|
|
300
|
+
output = whois_lookup(domain)
|
|
301
|
+
if output is None:
|
|
302
|
+
return "unknown"
|
|
303
|
+
|
|
304
|
+
# Check available first (specific patterns)
|
|
305
|
+
if AVAIL_PATTERNS.search(output):
|
|
306
|
+
# But make sure there's no actual domain data contradicting "not found"
|
|
307
|
+
# (some whois servers print "not found" in footer even for registered domains)
|
|
308
|
+
if not TAKEN_PATTERNS.search(output):
|
|
309
|
+
return "available"
|
|
310
|
+
|
|
311
|
+
# Check taken
|
|
312
|
+
if TAKEN_PATTERNS.search(output):
|
|
313
|
+
return "taken"
|
|
314
|
+
|
|
315
|
+
# Last resort: if we got substantial output (>500 chars) with no clear signal,
|
|
316
|
+
# it's likely a registered domain with unusual formatting
|
|
317
|
+
if len(output.strip()) > 500:
|
|
318
|
+
return "taken"
|
|
319
|
+
|
|
320
|
+
return "unknown"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ─── DOMAIN CHECK (COMBINED) ─────────────────────────────────
|
|
324
|
+
|
|
325
|
+
# TLDs that have NO RDAP support — skip RDAP entirely, go straight to whois
|
|
326
|
+
_NO_RDAP_TLDS = {"io", "co", "me", "sh", "so", "gg", "cc", "tv", "us", "uk", "eu",
|
|
327
|
+
"de", "fr", "it", "nl", "br", "jp", "ru", "cn", "ca", "au", "in"}
|
|
328
|
+
|
|
329
|
+
# TLDs where RDAP 404 is authoritative (the registry itself serves RDAP)
|
|
330
|
+
# For these, we trust 404 = available without whois double-check
|
|
331
|
+
_RDAP_AUTHORITATIVE_TLDS = {
|
|
332
|
+
"com", "net", "org", "ai", "dev", "app", "xyz", "tech", "cloud",
|
|
333
|
+
"info", "biz", "online", "site", "store",
|
|
334
|
+
# Google TLDs
|
|
335
|
+
"page", "how", "new", "day", "mov", "zip", "phd", "prof", "esq",
|
|
336
|
+
# Other well-known gTLDs
|
|
337
|
+
"blog", "shop", "art", "design", "agency", "studio", "media",
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def check_domain(domain: str) -> Tuple[str, str, Optional[Dict]]:
|
|
342
|
+
"""Check a single domain. Returns (status, method, rdap_data)."""
|
|
343
|
+
tld = domain.rsplit(".", 1)[-1].lower() if "." in domain else ""
|
|
344
|
+
|
|
345
|
+
# For TLDs with no RDAP, go straight to whois
|
|
346
|
+
if tld in _NO_RDAP_TLDS:
|
|
347
|
+
status = check_availability_whois(domain)
|
|
348
|
+
return status, "whois", None
|
|
349
|
+
|
|
350
|
+
# Try RDAP (queries registry directly, not rdap.org)
|
|
351
|
+
data, http_status = rdap_lookup(domain)
|
|
352
|
+
|
|
353
|
+
if http_status == 200 and data:
|
|
354
|
+
return "taken", "RDAP", data
|
|
355
|
+
|
|
356
|
+
if http_status == 404:
|
|
357
|
+
# For authoritative TLDs, trust RDAP 404 = available
|
|
358
|
+
if tld in _RDAP_AUTHORITATIVE_TLDS:
|
|
359
|
+
return "available", "RDAP", None
|
|
360
|
+
# For others, double-check with whois
|
|
361
|
+
whois_status = check_availability_whois(domain)
|
|
362
|
+
if whois_status == "taken":
|
|
363
|
+
return "taken", "whois", None
|
|
364
|
+
return "available", "RDAP+whois", None
|
|
365
|
+
|
|
366
|
+
# RDAP failed (rate limit, timeout, etc.) — fall back to whois
|
|
367
|
+
whois_status = check_availability_whois(domain)
|
|
368
|
+
if whois_status != "unknown":
|
|
369
|
+
return whois_status, "whois", None
|
|
370
|
+
|
|
371
|
+
# Both failed — try rdap.org as absolute last resort
|
|
372
|
+
fallback_url = f"{RDAP_FALLBACK}{domain}"
|
|
373
|
+
req = urllib.request.Request(fallback_url, headers={"Accept": "application/rdap+json,application/json"})
|
|
374
|
+
try:
|
|
375
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
376
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
377
|
+
return "taken", "RDAP", data
|
|
378
|
+
except urllib.error.HTTPError as e:
|
|
379
|
+
if e.code == 404:
|
|
380
|
+
return "available", "RDAP", None
|
|
381
|
+
except Exception:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
return "unknown", "?", None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ─── RDAP PARSERS ────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
def parse_rdap_events(data: Dict) -> Dict[str, str]:
|
|
390
|
+
"""Extract dates from RDAP events."""
|
|
391
|
+
dates = {}
|
|
392
|
+
for event in data.get("events", []):
|
|
393
|
+
action = event.get("eventAction", "")
|
|
394
|
+
date = event.get("eventDate", "")
|
|
395
|
+
if action and date:
|
|
396
|
+
dates[action] = date[:10]
|
|
397
|
+
return dates
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def parse_rdap_nameservers(data: Dict) -> List[str]:
|
|
401
|
+
"""Extract nameservers from RDAP data."""
|
|
402
|
+
ns_list = []
|
|
403
|
+
for ns in data.get("nameservers", []):
|
|
404
|
+
name = ns.get("ldhName", "")
|
|
405
|
+
if name:
|
|
406
|
+
ns_list.append(name.lower())
|
|
407
|
+
return ns_list
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def parse_rdap_registrar(data: Dict) -> str:
|
|
411
|
+
"""Extract registrar from RDAP entities."""
|
|
412
|
+
for entity in data.get("entities", []):
|
|
413
|
+
roles = entity.get("roles", [])
|
|
414
|
+
if "registrar" in roles:
|
|
415
|
+
vcard = entity.get("vcardArray", [])
|
|
416
|
+
if len(vcard) > 1:
|
|
417
|
+
for field in vcard[1]:
|
|
418
|
+
if field[0] == "fn":
|
|
419
|
+
return field[3]
|
|
420
|
+
for pid in entity.get("publicIds", []):
|
|
421
|
+
return pid.get("identifier", "")
|
|
422
|
+
handle = entity.get("handle", "")
|
|
423
|
+
if handle:
|
|
424
|
+
return handle
|
|
425
|
+
return "?"
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def parse_rdap_status(data: Dict) -> List[str]:
|
|
429
|
+
"""Extract status flags."""
|
|
430
|
+
return data.get("status", [])
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ─── COMMANDS ────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
def cmd_check(args):
|
|
436
|
+
"""Check domain availability."""
|
|
437
|
+
domains = [d.strip() for d in args.domains.split(",") if d.strip()]
|
|
438
|
+
if not domains:
|
|
439
|
+
print("ERROR: No domains provided.")
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
|
|
442
|
+
# Pre-load bootstrap once before parallel work
|
|
443
|
+
_load_bootstrap()
|
|
444
|
+
|
|
445
|
+
print(f"{'Domain':<45} {'Status':<15} {'Method'}")
|
|
446
|
+
print("-" * 72)
|
|
447
|
+
|
|
448
|
+
def check_one(domain):
|
|
449
|
+
status, method, _ = check_domain(domain)
|
|
450
|
+
return domain, status, method
|
|
451
|
+
|
|
452
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
453
|
+
futures = {pool.submit(check_one, d): d for d in domains}
|
|
454
|
+
results = []
|
|
455
|
+
for future in as_completed(futures):
|
|
456
|
+
results.append(future.result())
|
|
457
|
+
|
|
458
|
+
result_map = {r[0]: r for r in results}
|
|
459
|
+
for domain in domains:
|
|
460
|
+
if domain in result_map:
|
|
461
|
+
_, status, method = result_map[domain]
|
|
462
|
+
status_str = "AVAILABLE" if status == "available" else ("TAKEN" if status == "taken" else "UNKNOWN")
|
|
463
|
+
print(f"{domain:<45} {status_str:<15} {method}")
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def cmd_search(args):
|
|
467
|
+
"""Search keyword across TLDs."""
|
|
468
|
+
keyword = args.keyword.lower().strip()
|
|
469
|
+
if not keyword:
|
|
470
|
+
print("ERROR: No keyword provided.")
|
|
471
|
+
sys.exit(1)
|
|
472
|
+
|
|
473
|
+
tlds = [t.strip() for t in args.tlds.split(",")] if args.tlds else DEFAULT_TLDS
|
|
474
|
+
tlds = [t if t.startswith(".") else f".{t}" for t in tlds]
|
|
475
|
+
domains = [f"{keyword}{tld}" for tld in tlds]
|
|
476
|
+
|
|
477
|
+
# Pre-load bootstrap once before parallel work
|
|
478
|
+
_load_bootstrap()
|
|
479
|
+
|
|
480
|
+
print(f"Searching: {keyword}")
|
|
481
|
+
print(f"{'Domain':<45} {'Status':<15} {'Method'}")
|
|
482
|
+
print("-" * 72)
|
|
483
|
+
|
|
484
|
+
def check_one(domain):
|
|
485
|
+
status, method, _ = check_domain(domain)
|
|
486
|
+
return domain, status, method
|
|
487
|
+
|
|
488
|
+
# Process in batches — generous since we query registries directly now
|
|
489
|
+
batch_size = 8
|
|
490
|
+
all_results = []
|
|
491
|
+
for i in range(0, len(domains), batch_size):
|
|
492
|
+
batch = domains[i:i+batch_size]
|
|
493
|
+
with ThreadPoolExecutor(max_workers=6) as pool:
|
|
494
|
+
futures = {pool.submit(check_one, d): d for d in batch}
|
|
495
|
+
for future in as_completed(futures):
|
|
496
|
+
all_results.append(future.result())
|
|
497
|
+
if i + batch_size < len(domains):
|
|
498
|
+
time.sleep(0.2)
|
|
499
|
+
|
|
500
|
+
result_map = {r[0]: r for r in all_results}
|
|
501
|
+
avail_count = 0
|
|
502
|
+
for domain in domains:
|
|
503
|
+
if domain in result_map:
|
|
504
|
+
_, status, method = result_map[domain]
|
|
505
|
+
status_str = "AVAILABLE" if status == "available" else ("TAKEN" if status == "taken" else "UNKNOWN")
|
|
506
|
+
if status == "available":
|
|
507
|
+
avail_count += 1
|
|
508
|
+
print(f"{domain:<45} {status_str:<15} {method}")
|
|
509
|
+
|
|
510
|
+
print(f"\n{avail_count} available out of {len(domains)} checked")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def cmd_whois(args):
|
|
514
|
+
"""Full WHOIS/RDAP lookup for a domain."""
|
|
515
|
+
domain = args.domain.lower().strip()
|
|
516
|
+
|
|
517
|
+
# Pre-load bootstrap
|
|
518
|
+
_load_bootstrap()
|
|
519
|
+
|
|
520
|
+
# Try RDAP first
|
|
521
|
+
data, status = rdap_lookup(domain)
|
|
522
|
+
if status == 404:
|
|
523
|
+
print(f"{domain}: AVAILABLE (not registered)")
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
if status == 200 and data:
|
|
527
|
+
print(f"Domain: {data.get('ldhName', domain)}")
|
|
528
|
+
|
|
529
|
+
events = parse_rdap_events(data)
|
|
530
|
+
if "registration" in events:
|
|
531
|
+
print(f"Registered: {events['registration']}")
|
|
532
|
+
if "expiration" in events:
|
|
533
|
+
print(f"Expires: {events['expiration']}")
|
|
534
|
+
if "last changed" in events:
|
|
535
|
+
print(f"Updated: {events['last changed']}")
|
|
536
|
+
|
|
537
|
+
registrar = parse_rdap_registrar(data)
|
|
538
|
+
print(f"Registrar: {registrar}")
|
|
539
|
+
|
|
540
|
+
statuses = parse_rdap_status(data)
|
|
541
|
+
if statuses:
|
|
542
|
+
print(f"Status: {', '.join(statuses)}")
|
|
543
|
+
|
|
544
|
+
ns_list = parse_rdap_nameservers(data)
|
|
545
|
+
if ns_list:
|
|
546
|
+
print(f"Nameservers:")
|
|
547
|
+
for ns in ns_list:
|
|
548
|
+
print(f" {ns}")
|
|
549
|
+
|
|
550
|
+
for entity in data.get("entities", []):
|
|
551
|
+
roles = entity.get("roles", [])
|
|
552
|
+
vcard = entity.get("vcardArray", [])
|
|
553
|
+
if len(vcard) > 1 and roles:
|
|
554
|
+
role_str = "/".join(roles)
|
|
555
|
+
for field in vcard[1]:
|
|
556
|
+
if field[0] == "fn" and field[3]:
|
|
557
|
+
print(f"Contact ({role_str}): {field[3]}")
|
|
558
|
+
break
|
|
559
|
+
|
|
560
|
+
print(f"\nSource: RDAP")
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
# Fallback to whois CLI
|
|
564
|
+
output = whois_lookup(domain)
|
|
565
|
+
if output is None:
|
|
566
|
+
print(f"ERROR: Could not look up {domain} (whois not installed or timed out)")
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
if AVAIL_PATTERNS.search(output) and not TAKEN_PATTERNS.search(output):
|
|
570
|
+
print(f"{domain}: AVAILABLE (not registered)")
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
# Parse common fields from whois output
|
|
574
|
+
print(f"Domain: {domain}")
|
|
575
|
+
|
|
576
|
+
fields = [
|
|
577
|
+
("Registrar", r"Registrar:\s*(.+)"),
|
|
578
|
+
("Created", r"Creat(?:ion|ed)\s*(?:Date)?:\s*(.+)"),
|
|
579
|
+
("Expires", r"(?:Expir(?:y|ation)|Registry Expiry)\s*(?:Date)?:\s*(.+)"),
|
|
580
|
+
("Updated", r"Updated?\s*(?:Date)?:\s*(.+)"),
|
|
581
|
+
("Status", r"(?:Domain )?Status:\s*(.+)"),
|
|
582
|
+
]
|
|
583
|
+
|
|
584
|
+
seen_status = False
|
|
585
|
+
for label, pattern in fields:
|
|
586
|
+
matches = re.findall(pattern, output, re.IGNORECASE | re.MULTILINE)
|
|
587
|
+
if matches:
|
|
588
|
+
if label == "Status":
|
|
589
|
+
if not seen_status:
|
|
590
|
+
seen_status = True
|
|
591
|
+
for m in matches[:3]:
|
|
592
|
+
print(f"Status: {m.strip()}")
|
|
593
|
+
else:
|
|
594
|
+
print(f"{label + ':':<13}{matches[0].strip()}")
|
|
595
|
+
|
|
596
|
+
ns_matches = re.findall(r"Name Server:\s*(.+)", output, re.IGNORECASE)
|
|
597
|
+
if not ns_matches:
|
|
598
|
+
ns_matches = re.findall(r"nserver:\s*(.+)", output, re.IGNORECASE)
|
|
599
|
+
if ns_matches:
|
|
600
|
+
print(f"Nameservers:")
|
|
601
|
+
for ns in ns_matches[:6]:
|
|
602
|
+
print(f" {ns.strip().lower()}")
|
|
603
|
+
|
|
604
|
+
print(f"\nSource: whois CLI")
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def cmd_expiry(args):
|
|
608
|
+
"""Check domain expiration."""
|
|
609
|
+
domain = args.domain.lower().strip()
|
|
610
|
+
|
|
611
|
+
_load_bootstrap()
|
|
612
|
+
|
|
613
|
+
data, status = rdap_lookup(domain)
|
|
614
|
+
if status == 404:
|
|
615
|
+
print(f"{domain}: AVAILABLE (not registered)")
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
if status == 200 and data:
|
|
619
|
+
events = parse_rdap_events(data)
|
|
620
|
+
exp = events.get("expiration", "?")
|
|
621
|
+
reg = events.get("registration", "?")
|
|
622
|
+
print(f"Domain: {domain}")
|
|
623
|
+
print(f"Registered: {reg}")
|
|
624
|
+
print(f"Expires: {exp}")
|
|
625
|
+
|
|
626
|
+
if exp != "?":
|
|
627
|
+
try:
|
|
628
|
+
from datetime import datetime, date
|
|
629
|
+
exp_date = datetime.strptime(exp, "%Y-%m-%d").date()
|
|
630
|
+
today = date.today()
|
|
631
|
+
delta = (exp_date - today).days
|
|
632
|
+
if delta > 0:
|
|
633
|
+
print(f"Days left: {delta}")
|
|
634
|
+
elif delta == 0:
|
|
635
|
+
print(f"Days left: EXPIRES TODAY")
|
|
636
|
+
else:
|
|
637
|
+
print(f"Days left: EXPIRED {abs(delta)} days ago")
|
|
638
|
+
except Exception:
|
|
639
|
+
pass
|
|
640
|
+
return
|
|
641
|
+
|
|
642
|
+
# Whois fallback
|
|
643
|
+
output = whois_lookup(domain)
|
|
644
|
+
if output is None:
|
|
645
|
+
print(f"ERROR: Could not look up {domain}")
|
|
646
|
+
return
|
|
647
|
+
|
|
648
|
+
if AVAIL_PATTERNS.search(output) and not TAKEN_PATTERNS.search(output):
|
|
649
|
+
print(f"{domain}: AVAILABLE (not registered)")
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
print(f"Domain: {domain}")
|
|
653
|
+
exp_match = re.search(r"(?:Expir(?:y|ation)|Registry Expiry)\s*(?:Date)?:\s*(.+)", output, re.IGNORECASE)
|
|
654
|
+
if exp_match:
|
|
655
|
+
print(f"Expires: {exp_match.group(1).strip()}")
|
|
656
|
+
else:
|
|
657
|
+
print(f"Expires: ? (could not parse)")
|
|
658
|
+
|
|
659
|
+
reg_match = re.search(r"Creat(?:ion|ed)\s*(?:Date)?:\s*(.+)", output, re.IGNORECASE)
|
|
660
|
+
if reg_match:
|
|
661
|
+
print(f"Registered: {reg_match.group(1).strip()}")
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def cmd_nameservers(args):
|
|
665
|
+
"""Get nameservers for a domain."""
|
|
666
|
+
domain = args.domain.lower().strip()
|
|
667
|
+
|
|
668
|
+
_load_bootstrap()
|
|
669
|
+
|
|
670
|
+
data, status = rdap_lookup(domain)
|
|
671
|
+
if status == 404:
|
|
672
|
+
print(f"{domain}: AVAILABLE (not registered)")
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
if status == 200 and data:
|
|
676
|
+
ns_list = parse_rdap_nameservers(data)
|
|
677
|
+
if ns_list:
|
|
678
|
+
print(f"Nameservers for {domain}:")
|
|
679
|
+
for ns in ns_list:
|
|
680
|
+
print(f" {ns}")
|
|
681
|
+
else:
|
|
682
|
+
print(f"{domain}: No nameservers in RDAP data")
|
|
683
|
+
return
|
|
684
|
+
|
|
685
|
+
# Whois fallback
|
|
686
|
+
output = whois_lookup(domain)
|
|
687
|
+
if output is None:
|
|
688
|
+
print(f"ERROR: Could not look up {domain}")
|
|
689
|
+
return
|
|
690
|
+
|
|
691
|
+
if AVAIL_PATTERNS.search(output) and not TAKEN_PATTERNS.search(output):
|
|
692
|
+
print(f"{domain}: AVAILABLE (not registered)")
|
|
693
|
+
return
|
|
694
|
+
|
|
695
|
+
ns_matches = re.findall(r"Name Server:\s*(.+)", output, re.IGNORECASE)
|
|
696
|
+
if not ns_matches:
|
|
697
|
+
ns_matches = re.findall(r"nserver:\s*(.+)", output, re.IGNORECASE)
|
|
698
|
+
|
|
699
|
+
if ns_matches:
|
|
700
|
+
print(f"Nameservers for {domain}:")
|
|
701
|
+
for ns in ns_matches[:6]:
|
|
702
|
+
print(f" {ns.strip().lower()}")
|
|
703
|
+
else:
|
|
704
|
+
print(f"{domain}: Could not find nameservers")
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def cmd_bulk(args):
|
|
708
|
+
"""Check domains from a file (one per line)."""
|
|
709
|
+
filepath = args.file
|
|
710
|
+
if not os.path.isfile(filepath):
|
|
711
|
+
print(f"ERROR: File not found: {filepath}")
|
|
712
|
+
sys.exit(1)
|
|
713
|
+
|
|
714
|
+
with open(filepath, "r") as f:
|
|
715
|
+
domains = [line.strip() for line in f if line.strip() and not line.startswith("#")]
|
|
716
|
+
|
|
717
|
+
if not domains:
|
|
718
|
+
print("ERROR: No domains in file.")
|
|
719
|
+
sys.exit(1)
|
|
720
|
+
|
|
721
|
+
_load_bootstrap()
|
|
722
|
+
|
|
723
|
+
print(f"Checking {len(domains)} domains from {filepath}")
|
|
724
|
+
print(f"{'Domain':<45} {'Status':<15} {'Method'}")
|
|
725
|
+
print("-" * 72)
|
|
726
|
+
|
|
727
|
+
avail_count = 0
|
|
728
|
+
taken_count = 0
|
|
729
|
+
|
|
730
|
+
def check_one(domain):
|
|
731
|
+
status, method, _ = check_domain(domain)
|
|
732
|
+
return domain, status, method
|
|
733
|
+
|
|
734
|
+
batch_size = 10
|
|
735
|
+
all_results = []
|
|
736
|
+
for i in range(0, len(domains), batch_size):
|
|
737
|
+
batch = domains[i:i+batch_size]
|
|
738
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
739
|
+
futures = {pool.submit(check_one, d): d for d in batch}
|
|
740
|
+
for future in as_completed(futures):
|
|
741
|
+
all_results.append(future.result())
|
|
742
|
+
if i + batch_size < len(domains):
|
|
743
|
+
time.sleep(0.3)
|
|
744
|
+
|
|
745
|
+
result_map = {r[0]: r for r in all_results}
|
|
746
|
+
for domain in domains:
|
|
747
|
+
if domain in result_map:
|
|
748
|
+
_, status, method = result_map[domain]
|
|
749
|
+
status_str = "AVAILABLE" if status == "available" else ("TAKEN" if status == "taken" else "UNKNOWN")
|
|
750
|
+
if status == "available":
|
|
751
|
+
avail_count += 1
|
|
752
|
+
elif status == "taken":
|
|
753
|
+
taken_count += 1
|
|
754
|
+
print(f"{domain:<45} {status_str:<15} {method}")
|
|
755
|
+
|
|
756
|
+
print(f"\nSummary: {avail_count} available, {taken_count} taken, {len(domains) - avail_count - taken_count} unknown")
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
# ─── MAIN ────────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
def main():
|
|
762
|
+
parser = argparse.ArgumentParser(
|
|
763
|
+
description="Domain Research CLI -- Free RDAP + WHOIS lookup, zero credentials",
|
|
764
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
765
|
+
)
|
|
766
|
+
subs = parser.add_subparsers(dest="command")
|
|
767
|
+
|
|
768
|
+
p = subs.add_parser("check", help="Check domain availability")
|
|
769
|
+
p.add_argument("domains", help="Comma-separated domains")
|
|
770
|
+
|
|
771
|
+
p = subs.add_parser("search", help="Search keyword across TLDs")
|
|
772
|
+
p.add_argument("keyword", help="Keyword to search")
|
|
773
|
+
p.add_argument("--tlds", default=None, help="Comma-separated TLDs (e.g. .com,.net,.io)")
|
|
774
|
+
|
|
775
|
+
p = subs.add_parser("whois", help="Full WHOIS/RDAP lookup")
|
|
776
|
+
p.add_argument("domain", help="Domain to look up")
|
|
777
|
+
|
|
778
|
+
p = subs.add_parser("expiry", help="Check domain expiration")
|
|
779
|
+
p.add_argument("domain", help="Domain to check")
|
|
780
|
+
|
|
781
|
+
p = subs.add_parser("nameservers", help="Get nameservers")
|
|
782
|
+
p.add_argument("domain", help="Domain to check")
|
|
783
|
+
|
|
784
|
+
p = subs.add_parser("bulk", help="Bulk check from file")
|
|
785
|
+
p.add_argument("file", help="File with one domain per line")
|
|
786
|
+
|
|
787
|
+
args = parser.parse_args()
|
|
788
|
+
|
|
789
|
+
if not args.command:
|
|
790
|
+
parser.print_help()
|
|
791
|
+
sys.exit(1)
|
|
792
|
+
|
|
793
|
+
cmd_map = {
|
|
794
|
+
"check": cmd_check,
|
|
795
|
+
"search": cmd_search,
|
|
796
|
+
"whois": cmd_whois,
|
|
797
|
+
"expiry": cmd_expiry,
|
|
798
|
+
"nameservers": cmd_nameservers,
|
|
799
|
+
"bulk": cmd_bulk,
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
func = cmd_map.get(args.command)
|
|
803
|
+
if func:
|
|
804
|
+
func(args)
|
|
805
|
+
else:
|
|
806
|
+
parser.print_help()
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
if __name__ == "__main__":
|
|
810
|
+
main()
|