domainstorm 0.2.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/LICENSE +21 -0
- package/README.md +99 -0
- package/agent-md-candidates.txt +117 -0
- package/check-md-domains.mjs +641 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanishq Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Domainstorm CLI
|
|
2
|
+
|
|
3
|
+
`domainstorm` is a domain brainstorming + availability CLI for agent products.
|
|
4
|
+
One command can generate naming candidates and check registration status via WHOIS.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
From GitHub (works now):
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm i -g github:tanishqsh/domain-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
From npm (after publish):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i -g domainstorm
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Run without installing globally:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx --yes github:tanishqsh/domain-cli#main --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## One-Command Brainstorm
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
domainstorm --brainstorm "agent cli" "mcp broker" --tld md --server whois.nic.md --only-available
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This command:
|
|
33
|
+
- Generates brandable name candidates from your seed phrases
|
|
34
|
+
- Appends your target TLD
|
|
35
|
+
- Checks registration status
|
|
36
|
+
- Prints a narrative hint (`story=...`) for each candidate
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
domainstorm openai.com example.org --output /tmp/domain-results.csv
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
domainstorm --input agent-md-candidates.txt --tld md --server whois.nic.md --output /tmp/md-results.csv
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`domain-check` is kept as a compatibility alias.
|
|
49
|
+
|
|
50
|
+
## Input Format
|
|
51
|
+
|
|
52
|
+
- One label/domain per line in a text file
|
|
53
|
+
- `#` comments are ignored
|
|
54
|
+
- Labels without a TLD are converted to `<label>.<tld>` (default: `md`)
|
|
55
|
+
- Comma-separated values are accepted per line
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
|
|
59
|
+
```txt
|
|
60
|
+
# sample
|
|
61
|
+
broker
|
|
62
|
+
agenthub.md
|
|
63
|
+
mcpbroker,mcprouter
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Output Columns
|
|
67
|
+
|
|
68
|
+
- `domain`
|
|
69
|
+
- `status`: `registered`, `likely_available`, or `unknown`
|
|
70
|
+
- `reason`: why the status was chosen
|
|
71
|
+
- `legal_risk`: heuristic trademark risk flag (`low` / `high_tm_risk`)
|
|
72
|
+
- `story`: brainstorm narrative tag for generated names
|
|
73
|
+
- `error`: lookup/system failures when present
|
|
74
|
+
|
|
75
|
+
## CLI Flags
|
|
76
|
+
|
|
77
|
+
- `--input <file>`
|
|
78
|
+
- `--output <file>`
|
|
79
|
+
- `--tld <tld>` default `md`
|
|
80
|
+
- `--server <whois-host>` optional (for example `whois.nic.md`)
|
|
81
|
+
- `--concurrency <n>`
|
|
82
|
+
- `--timeout-ms <n>`
|
|
83
|
+
- `--brainstorm` / `--storm`
|
|
84
|
+
- `--max-suggestions <n>` default `120`
|
|
85
|
+
- `--only-available`
|
|
86
|
+
- `--raw`
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- WHOIS formats vary by registry; treat `likely_available` as a pre-check, not final registrar confirmation.
|
|
91
|
+
- Registries can rate-limit bulk lookups; retry unknown rows after cooldown.
|
|
92
|
+
- `whois` must be installed locally (`brew install whois` on macOS).
|
|
93
|
+
|
|
94
|
+
## Publish To npm
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm login
|
|
98
|
+
npm publish --access public
|
|
99
|
+
```
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Generic agent/AI-adjacent names (avoid trademarked brand terms)
|
|
2
|
+
agent
|
|
3
|
+
agents
|
|
4
|
+
agentic
|
|
5
|
+
broker
|
|
6
|
+
router
|
|
7
|
+
gateway
|
|
8
|
+
mesh
|
|
9
|
+
hub
|
|
10
|
+
stack
|
|
11
|
+
runtime
|
|
12
|
+
engine
|
|
13
|
+
studio
|
|
14
|
+
labs
|
|
15
|
+
forge
|
|
16
|
+
protocol
|
|
17
|
+
tooling
|
|
18
|
+
tool
|
|
19
|
+
tools
|
|
20
|
+
plugin
|
|
21
|
+
plugins
|
|
22
|
+
automation
|
|
23
|
+
workflow
|
|
24
|
+
workflows
|
|
25
|
+
pipeline
|
|
26
|
+
pipelines
|
|
27
|
+
memory
|
|
28
|
+
context
|
|
29
|
+
prompt
|
|
30
|
+
prompts
|
|
31
|
+
rag
|
|
32
|
+
vector
|
|
33
|
+
vectors
|
|
34
|
+
embedding
|
|
35
|
+
embeddings
|
|
36
|
+
inference
|
|
37
|
+
eval
|
|
38
|
+
evals
|
|
39
|
+
benchmark
|
|
40
|
+
guardrails
|
|
41
|
+
safety
|
|
42
|
+
security
|
|
43
|
+
audit
|
|
44
|
+
monitor
|
|
45
|
+
observability
|
|
46
|
+
telemetry
|
|
47
|
+
trace
|
|
48
|
+
search
|
|
49
|
+
retrieval
|
|
50
|
+
knowledge
|
|
51
|
+
graph
|
|
52
|
+
scheduler
|
|
53
|
+
cache
|
|
54
|
+
compute
|
|
55
|
+
gpu
|
|
56
|
+
token
|
|
57
|
+
tokens
|
|
58
|
+
agenthub
|
|
59
|
+
agentstack
|
|
60
|
+
agentcloud
|
|
61
|
+
agentmesh
|
|
62
|
+
agentruntime
|
|
63
|
+
agentengine
|
|
64
|
+
agentstudio
|
|
65
|
+
agentforge
|
|
66
|
+
agentflow
|
|
67
|
+
agentops
|
|
68
|
+
agentinfra
|
|
69
|
+
agentprotocol
|
|
70
|
+
agentregistry
|
|
71
|
+
agentdirectory
|
|
72
|
+
agentmarket
|
|
73
|
+
agentstore
|
|
74
|
+
agenthosting
|
|
75
|
+
agentdeploy
|
|
76
|
+
agentsecurity
|
|
77
|
+
agentguard
|
|
78
|
+
agentaudit
|
|
79
|
+
agenttrace
|
|
80
|
+
agentmonitor
|
|
81
|
+
agentmemory
|
|
82
|
+
agentcontext
|
|
83
|
+
agentsearch
|
|
84
|
+
agentrag
|
|
85
|
+
agentvector
|
|
86
|
+
agenteval
|
|
87
|
+
agentbench
|
|
88
|
+
agentapi
|
|
89
|
+
agentsdk
|
|
90
|
+
agenttools
|
|
91
|
+
agentplugin
|
|
92
|
+
agentintegrations
|
|
93
|
+
agentconnectors
|
|
94
|
+
agentnetwork
|
|
95
|
+
agentgateway
|
|
96
|
+
agentrouter
|
|
97
|
+
agentbroker
|
|
98
|
+
mcp
|
|
99
|
+
mcpbroker
|
|
100
|
+
mcprouter
|
|
101
|
+
mcpgateway
|
|
102
|
+
mcphub
|
|
103
|
+
mcpregistry
|
|
104
|
+
mcpdirectory
|
|
105
|
+
mcpmarket
|
|
106
|
+
mcptools
|
|
107
|
+
mcpcloud
|
|
108
|
+
mcpstack
|
|
109
|
+
mcpstudio
|
|
110
|
+
mcpruntime
|
|
111
|
+
mcpsecurity
|
|
112
|
+
mcpaudit
|
|
113
|
+
mcpmonitor
|
|
114
|
+
mcpops
|
|
115
|
+
mcpmemory
|
|
116
|
+
mcpcontext
|
|
117
|
+
mcpeval
|
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONCURRENCY = 6;
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 12000;
|
|
9
|
+
const DEFAULT_TLD = "md";
|
|
10
|
+
const DEFAULT_MAX_SUGGESTIONS = 120;
|
|
11
|
+
const AVAILABILITY_PATTERNS = [
|
|
12
|
+
"no match for",
|
|
13
|
+
"not found",
|
|
14
|
+
"no entries found",
|
|
15
|
+
"domain not found",
|
|
16
|
+
"no data found",
|
|
17
|
+
"no object found",
|
|
18
|
+
"object does not exist",
|
|
19
|
+
"no such domain",
|
|
20
|
+
"status: free",
|
|
21
|
+
"status: available",
|
|
22
|
+
"available",
|
|
23
|
+
];
|
|
24
|
+
const REGISTRATION_PATTERNS = [
|
|
25
|
+
"domain name:",
|
|
26
|
+
"domain name:",
|
|
27
|
+
"domain:",
|
|
28
|
+
"registry domain id:",
|
|
29
|
+
"whois:",
|
|
30
|
+
"registrar:",
|
|
31
|
+
"creation date:",
|
|
32
|
+
"expiry date:",
|
|
33
|
+
"expiration date:",
|
|
34
|
+
"registered on:",
|
|
35
|
+
"expires on:",
|
|
36
|
+
"domain state:",
|
|
37
|
+
"nameserver:",
|
|
38
|
+
];
|
|
39
|
+
const RATE_LIMIT_PATTERNS = [
|
|
40
|
+
"limit exceeded",
|
|
41
|
+
"too many requests",
|
|
42
|
+
"try again later",
|
|
43
|
+
"rate limit",
|
|
44
|
+
"blocked",
|
|
45
|
+
];
|
|
46
|
+
const LOOKUP_ERROR_PATTERNS = [
|
|
47
|
+
"nodename nor servname provided",
|
|
48
|
+
"temporary failure in name resolution",
|
|
49
|
+
"name or service not known",
|
|
50
|
+
"connection timed out",
|
|
51
|
+
"unable to connect",
|
|
52
|
+
"no route to host",
|
|
53
|
+
"network is unreachable",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const STOP_WORDS = new Set([
|
|
57
|
+
"a",
|
|
58
|
+
"an",
|
|
59
|
+
"the",
|
|
60
|
+
"and",
|
|
61
|
+
"or",
|
|
62
|
+
"for",
|
|
63
|
+
"to",
|
|
64
|
+
"of",
|
|
65
|
+
"by",
|
|
66
|
+
"with",
|
|
67
|
+
"on",
|
|
68
|
+
"in",
|
|
69
|
+
"at",
|
|
70
|
+
"from",
|
|
71
|
+
"your",
|
|
72
|
+
"our",
|
|
73
|
+
"my",
|
|
74
|
+
"agent",
|
|
75
|
+
"agents",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const DEFAULT_STORM_SEEDS = [
|
|
79
|
+
"agent",
|
|
80
|
+
"mcp",
|
|
81
|
+
"ai",
|
|
82
|
+
"broker",
|
|
83
|
+
"router",
|
|
84
|
+
"runtime",
|
|
85
|
+
"ops",
|
|
86
|
+
"stack",
|
|
87
|
+
"forge",
|
|
88
|
+
"mesh",
|
|
89
|
+
"pilot",
|
|
90
|
+
"flow",
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const STORM_SUFFIXES = [
|
|
94
|
+
"storm",
|
|
95
|
+
"ops",
|
|
96
|
+
"hub",
|
|
97
|
+
"mesh",
|
|
98
|
+
"forge",
|
|
99
|
+
"stack",
|
|
100
|
+
"pilot",
|
|
101
|
+
"route",
|
|
102
|
+
"router",
|
|
103
|
+
"broker",
|
|
104
|
+
"runtime",
|
|
105
|
+
"engine",
|
|
106
|
+
"works",
|
|
107
|
+
"cloud",
|
|
108
|
+
"grid",
|
|
109
|
+
"terminal",
|
|
110
|
+
"dock",
|
|
111
|
+
"labs",
|
|
112
|
+
"studio",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const STORM_PREFIXES = [
|
|
116
|
+
"agent",
|
|
117
|
+
"ai",
|
|
118
|
+
"mcp",
|
|
119
|
+
"task",
|
|
120
|
+
"flow",
|
|
121
|
+
"auto",
|
|
122
|
+
"tool",
|
|
123
|
+
"prompt",
|
|
124
|
+
"context",
|
|
125
|
+
"trace",
|
|
126
|
+
"guard",
|
|
127
|
+
"secure",
|
|
128
|
+
"fleet",
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// Small, non-authoritative heuristic list for quick legal-risk triage.
|
|
132
|
+
const TRADEMARK_KEYWORDS = [
|
|
133
|
+
"openai",
|
|
134
|
+
"chatgpt",
|
|
135
|
+
"anthropic",
|
|
136
|
+
"claude",
|
|
137
|
+
"google",
|
|
138
|
+
"gemini",
|
|
139
|
+
"microsoft",
|
|
140
|
+
"copilot",
|
|
141
|
+
"meta",
|
|
142
|
+
"llama",
|
|
143
|
+
"apple",
|
|
144
|
+
"amazon",
|
|
145
|
+
"aws",
|
|
146
|
+
"xai",
|
|
147
|
+
"grok",
|
|
148
|
+
"nvidia",
|
|
149
|
+
"intel",
|
|
150
|
+
"samsung",
|
|
151
|
+
"tesla",
|
|
152
|
+
"spacex",
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
function printUsage() {
|
|
156
|
+
console.log(
|
|
157
|
+
[
|
|
158
|
+
"Usage:",
|
|
159
|
+
" domainstorm --input domains.txt [--output results.csv]",
|
|
160
|
+
" domainstorm openai.com example.org",
|
|
161
|
+
" domainstorm --brainstorm \"agent cli\" \"mcp broker\" --tld md --server whois.nic.md",
|
|
162
|
+
"",
|
|
163
|
+
"Options:",
|
|
164
|
+
" --input <file> Text file of domains/labels (one per line, '#' comments supported)",
|
|
165
|
+
" --output <file> Optional CSV output path",
|
|
166
|
+
" --tld <tld> Default TLD for bare labels (default: md)",
|
|
167
|
+
" --server <host> Optional WHOIS server (ex: whois.nic.md)",
|
|
168
|
+
" --concurrency <n> Parallel WHOIS requests (default: 6)",
|
|
169
|
+
" --timeout-ms <n> WHOIS timeout per domain (default: 12000)",
|
|
170
|
+
" --brainstorm Generate domain ideas from seeds, then check availability",
|
|
171
|
+
" --max-suggestions <n> Max brainstormed candidates (default: 120)",
|
|
172
|
+
" --only-available Print only likely available domains",
|
|
173
|
+
" --raw Include WHOIS snippet in console output",
|
|
174
|
+
" --help Show this help",
|
|
175
|
+
"",
|
|
176
|
+
"Notes:",
|
|
177
|
+
" - Labels without a dot are auto-converted to <label>.<tld>",
|
|
178
|
+
" - WHOIS-based checks are heuristic; always confirm at registrar checkout",
|
|
179
|
+
].join("\n"),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseArgs(argv) {
|
|
184
|
+
const options = {
|
|
185
|
+
input: null,
|
|
186
|
+
output: null,
|
|
187
|
+
concurrency: DEFAULT_CONCURRENCY,
|
|
188
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
189
|
+
tld: DEFAULT_TLD,
|
|
190
|
+
server: null,
|
|
191
|
+
brainstorm: false,
|
|
192
|
+
maxSuggestions: DEFAULT_MAX_SUGGESTIONS,
|
|
193
|
+
onlyAvailable: false,
|
|
194
|
+
raw: false,
|
|
195
|
+
labels: [],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
199
|
+
const arg = argv[i];
|
|
200
|
+
switch (arg) {
|
|
201
|
+
case "--input":
|
|
202
|
+
case "-i":
|
|
203
|
+
options.input = argv[++i];
|
|
204
|
+
break;
|
|
205
|
+
case "--output":
|
|
206
|
+
case "-o":
|
|
207
|
+
options.output = argv[++i];
|
|
208
|
+
break;
|
|
209
|
+
case "--concurrency":
|
|
210
|
+
case "-c":
|
|
211
|
+
options.concurrency = Number.parseInt(argv[++i], 10);
|
|
212
|
+
break;
|
|
213
|
+
case "--tld":
|
|
214
|
+
options.tld = argv[++i];
|
|
215
|
+
break;
|
|
216
|
+
case "--server":
|
|
217
|
+
options.server = argv[++i];
|
|
218
|
+
break;
|
|
219
|
+
case "--timeout-ms":
|
|
220
|
+
options.timeoutMs = Number.parseInt(argv[++i], 10);
|
|
221
|
+
break;
|
|
222
|
+
case "--brainstorm":
|
|
223
|
+
case "--storm":
|
|
224
|
+
options.brainstorm = true;
|
|
225
|
+
break;
|
|
226
|
+
case "--max-suggestions":
|
|
227
|
+
options.maxSuggestions = Number.parseInt(argv[++i], 10);
|
|
228
|
+
break;
|
|
229
|
+
case "--only-available":
|
|
230
|
+
options.onlyAvailable = true;
|
|
231
|
+
break;
|
|
232
|
+
case "--raw":
|
|
233
|
+
options.raw = true;
|
|
234
|
+
break;
|
|
235
|
+
case "--help":
|
|
236
|
+
case "-h":
|
|
237
|
+
printUsage();
|
|
238
|
+
process.exit(0);
|
|
239
|
+
default:
|
|
240
|
+
if (arg.startsWith("-")) {
|
|
241
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
242
|
+
}
|
|
243
|
+
if (arg === "storm" || arg === "brainstorm") {
|
|
244
|
+
options.brainstorm = true;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
options.labels.push(arg);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!options.input && options.labels.length === 0 && !options.brainstorm) {
|
|
252
|
+
throw new Error("Provide --input <file>, one/more labels/domains, or use --brainstorm.");
|
|
253
|
+
}
|
|
254
|
+
if (!Number.isInteger(options.concurrency) || options.concurrency < 1 || options.concurrency > 50) {
|
|
255
|
+
throw new Error("--concurrency must be an integer between 1 and 50.");
|
|
256
|
+
}
|
|
257
|
+
if (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1000 || options.timeoutMs > 60000) {
|
|
258
|
+
throw new Error("--timeout-ms must be an integer between 1000 and 60000.");
|
|
259
|
+
}
|
|
260
|
+
if (!Number.isInteger(options.maxSuggestions) || options.maxSuggestions < 10 || options.maxSuggestions > 1000) {
|
|
261
|
+
throw new Error("--max-suggestions must be an integer between 10 and 1000.");
|
|
262
|
+
}
|
|
263
|
+
if (options.tld !== null && options.tld !== undefined) {
|
|
264
|
+
options.tld = options.tld.trim().toLowerCase().replace(/^\./, "");
|
|
265
|
+
if (!options.tld || !/^[a-z0-9-]{2,63}$/.test(options.tld)) {
|
|
266
|
+
throw new Error("--tld must be a valid TLD label such as 'md', 'com', or 'io'.");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (options.server !== null && options.server !== undefined) {
|
|
270
|
+
options.server = options.server.trim().toLowerCase();
|
|
271
|
+
if (!options.server || /[^a-z0-9.-]/.test(options.server)) {
|
|
272
|
+
throw new Error("--server must be a valid host name such as 'whois.nic.md'.");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return options;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function loadLabels(options) {
|
|
280
|
+
const values = [...options.labels];
|
|
281
|
+
if (options.input) {
|
|
282
|
+
const fullPath = path.resolve(options.input);
|
|
283
|
+
const file = await fs.readFile(fullPath, "utf8");
|
|
284
|
+
for (const rawLine of file.split(/\r?\n/)) {
|
|
285
|
+
const line = rawLine.trim();
|
|
286
|
+
if (!line || line.startsWith("#")) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// Support comma-separated rows as well.
|
|
290
|
+
for (const cell of line.split(",")) {
|
|
291
|
+
const cleaned = cell.trim();
|
|
292
|
+
if (cleaned) {
|
|
293
|
+
values.push(cleaned);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return values;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeDomain(value, defaultTld) {
|
|
302
|
+
let v = value.trim().toLowerCase();
|
|
303
|
+
v = v.replace(/^https?:\/\//, "");
|
|
304
|
+
v = v.split("/")[0];
|
|
305
|
+
v = v.replace(/\.$/, "");
|
|
306
|
+
if (!v.includes(".") && defaultTld) {
|
|
307
|
+
v = `${v}.${defaultTld}`;
|
|
308
|
+
}
|
|
309
|
+
return v;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeLabel(value) {
|
|
313
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isValidDomain(domain) {
|
|
317
|
+
return /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,63}$/.test(domain);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function legalRisk(domain) {
|
|
321
|
+
const core = domain.slice(0, domain.lastIndexOf("."));
|
|
322
|
+
for (const keyword of TRADEMARK_KEYWORDS) {
|
|
323
|
+
if (core.includes(keyword)) {
|
|
324
|
+
return "high_tm_risk";
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return "low";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function tokenizeSeeds(rawValues) {
|
|
331
|
+
const tokens = [];
|
|
332
|
+
for (const value of rawValues) {
|
|
333
|
+
let v = value.trim().toLowerCase();
|
|
334
|
+
if (!v) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
v = v.replace(/^https?:\/\//, "");
|
|
338
|
+
v = v.split("/")[0];
|
|
339
|
+
v = v.replace(/\.[a-z0-9.-]+$/i, "");
|
|
340
|
+
for (const token of v.split(/[^a-z0-9]+/)) {
|
|
341
|
+
if (!token || token.length < 2 || STOP_WORDS.has(token)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
tokens.push(token);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return tokens;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function brainstormCandidates(rawSeeds, maxSuggestions) {
|
|
351
|
+
const seedTokens = tokenizeSeeds(rawSeeds);
|
|
352
|
+
const seedPool = Array.from(new Set([...seedTokens, ...DEFAULT_STORM_SEEDS])).slice(0, 24);
|
|
353
|
+
const scored = new Map();
|
|
354
|
+
|
|
355
|
+
function add(label, story, bonus = 0) {
|
|
356
|
+
const normalized = normalizeLabel(label);
|
|
357
|
+
if (!normalized || normalized.length < 3 || normalized.length > 63) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!/^[a-z0-9]+$/.test(normalized)) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const lengthPenalty = normalized.length;
|
|
364
|
+
const score = 1000 - lengthPenalty * 7 + bonus;
|
|
365
|
+
const existing = scored.get(normalized);
|
|
366
|
+
if (!existing || score > existing.score) {
|
|
367
|
+
scored.set(normalized, { label: normalized, score, story });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const seed of seedPool) {
|
|
372
|
+
add(seed, "Keyword brand");
|
|
373
|
+
for (const suffix of STORM_SUFFIXES) {
|
|
374
|
+
add(`${seed}${suffix}`, "Keyword + product framing", seed.length < 8 ? 25 : 10);
|
|
375
|
+
}
|
|
376
|
+
for (const prefix of STORM_PREFIXES) {
|
|
377
|
+
add(`${prefix}${seed}`, "Platform + keyword framing", 10);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (let i = 0; i < seedPool.length; i += 1) {
|
|
382
|
+
for (let j = i + 1; j < seedPool.length; j += 1) {
|
|
383
|
+
const a = seedPool[i];
|
|
384
|
+
const b = seedPool[j];
|
|
385
|
+
add(`${a}${b}`, "Two-keyword compound", 20);
|
|
386
|
+
add(`${b}${a}`, "Two-keyword compound", 20);
|
|
387
|
+
add(`${a}${b}hq`, "Company-style compound", 10);
|
|
388
|
+
add(`${a}${b}labs`, "Innovation-style compound", 8);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const sorted = Array.from(scored.values())
|
|
393
|
+
.sort((a, b) => b.score - a.score || a.label.localeCompare(b.label))
|
|
394
|
+
.slice(0, maxSuggestions);
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
labels: sorted.map((item) => item.label),
|
|
398
|
+
storyByLabel: new Map(sorted.map((item) => [item.label, item.story])),
|
|
399
|
+
usedSeeds: seedTokens.length > 0 ? Array.from(new Set(seedTokens)) : DEFAULT_STORM_SEEDS,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function classifyWhois(raw) {
|
|
404
|
+
const text = raw.toLowerCase();
|
|
405
|
+
if (!text.trim()) {
|
|
406
|
+
return { status: "unknown", reason: "empty_whois_response" };
|
|
407
|
+
}
|
|
408
|
+
if (LOOKUP_ERROR_PATTERNS.some((p) => text.includes(p))) {
|
|
409
|
+
return { status: "unknown", reason: "whois_lookup_failed" };
|
|
410
|
+
}
|
|
411
|
+
if (RATE_LIMIT_PATTERNS.some((p) => text.includes(p))) {
|
|
412
|
+
return { status: "unknown", reason: "rate_limited_or_blocked" };
|
|
413
|
+
}
|
|
414
|
+
if (AVAILABILITY_PATTERNS.some((p) => text.includes(p))) {
|
|
415
|
+
return { status: "likely_available", reason: "availability_pattern_match" };
|
|
416
|
+
}
|
|
417
|
+
if (REGISTRATION_PATTERNS.some((p) => text.includes(p))) {
|
|
418
|
+
return { status: "registered", reason: "registration_pattern_match" };
|
|
419
|
+
}
|
|
420
|
+
return { status: "unknown", reason: "unrecognized_whois_format" };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function runWhois(domain, timeoutMs, server) {
|
|
424
|
+
return new Promise((resolve) => {
|
|
425
|
+
const args = server ? ["-h", server, domain] : [domain];
|
|
426
|
+
const child = spawn("whois", args, {
|
|
427
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
let stdout = "";
|
|
431
|
+
let stderr = "";
|
|
432
|
+
let done = false;
|
|
433
|
+
|
|
434
|
+
const timer = setTimeout(() => {
|
|
435
|
+
if (!done) {
|
|
436
|
+
done = true;
|
|
437
|
+
child.kill("SIGKILL");
|
|
438
|
+
resolve({
|
|
439
|
+
domain,
|
|
440
|
+
status: "unknown",
|
|
441
|
+
reason: "whois_timeout",
|
|
442
|
+
raw: "",
|
|
443
|
+
error: `whois timed out after ${timeoutMs} ms`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}, timeoutMs);
|
|
447
|
+
|
|
448
|
+
child.stdout.on("data", (chunk) => {
|
|
449
|
+
stdout += chunk.toString();
|
|
450
|
+
});
|
|
451
|
+
child.stderr.on("data", (chunk) => {
|
|
452
|
+
stderr += chunk.toString();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
child.on("error", (err) => {
|
|
456
|
+
if (done) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
done = true;
|
|
460
|
+
clearTimeout(timer);
|
|
461
|
+
resolve({
|
|
462
|
+
domain,
|
|
463
|
+
status: "unknown",
|
|
464
|
+
reason: "whois_error",
|
|
465
|
+
raw: "",
|
|
466
|
+
error: err.message,
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
child.on("close", () => {
|
|
471
|
+
if (done) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
done = true;
|
|
475
|
+
clearTimeout(timer);
|
|
476
|
+
const merged = [stdout, stderr].filter(Boolean).join("\n");
|
|
477
|
+
const classification = classifyWhois(merged);
|
|
478
|
+
const stderrText = stderr.trim();
|
|
479
|
+
resolve({
|
|
480
|
+
domain,
|
|
481
|
+
status: classification.status,
|
|
482
|
+
reason: classification.reason,
|
|
483
|
+
raw: merged.trim(),
|
|
484
|
+
error: classification.reason === "whois_lookup_failed" ? stderrText || "WHOIS lookup failed" : null,
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function mapLimit(items, limit, iterator) {
|
|
491
|
+
const results = new Array(items.length);
|
|
492
|
+
let index = 0;
|
|
493
|
+
|
|
494
|
+
async function worker() {
|
|
495
|
+
for (;;) {
|
|
496
|
+
const current = index;
|
|
497
|
+
index += 1;
|
|
498
|
+
if (current >= items.length) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
results[current] = await iterator(items[current], current);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
506
|
+
await Promise.all(workers);
|
|
507
|
+
return results;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function csvEscape(value) {
|
|
511
|
+
const text = String(value ?? "");
|
|
512
|
+
if (/[",\n]/.test(text)) {
|
|
513
|
+
return `"${text.replaceAll('"', '""')}"`;
|
|
514
|
+
}
|
|
515
|
+
return text;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function maybeWriteCsv(results, outputPath) {
|
|
519
|
+
if (!outputPath) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const header = ["domain", "status", "reason", "legal_risk", "story", "error"];
|
|
523
|
+
const lines = [header.join(",")];
|
|
524
|
+
for (const row of results) {
|
|
525
|
+
lines.push(
|
|
526
|
+
[
|
|
527
|
+
row.domain,
|
|
528
|
+
row.status,
|
|
529
|
+
row.reason,
|
|
530
|
+
row.legalRisk,
|
|
531
|
+
row.story ?? "",
|
|
532
|
+
row.error ?? "",
|
|
533
|
+
]
|
|
534
|
+
.map(csvEscape)
|
|
535
|
+
.join(","),
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
const absolute = path.resolve(outputPath);
|
|
539
|
+
await fs.writeFile(absolute, `${lines.join("\n")}\n`, "utf8");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function summarize(results) {
|
|
543
|
+
const counters = new Map();
|
|
544
|
+
for (const row of results) {
|
|
545
|
+
counters.set(row.status, (counters.get(row.status) ?? 0) + 1);
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
likelyAvailable: counters.get("likely_available") ?? 0,
|
|
549
|
+
registered: counters.get("registered") ?? 0,
|
|
550
|
+
unknown: counters.get("unknown") ?? 0,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function snippet(raw) {
|
|
555
|
+
if (!raw) {
|
|
556
|
+
return "";
|
|
557
|
+
}
|
|
558
|
+
return raw.replace(/\s+/g, " ").slice(0, 120);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function main() {
|
|
562
|
+
const options = parseArgs(process.argv.slice(2));
|
|
563
|
+
let rawLabels = await loadLabels(options);
|
|
564
|
+
let storyByLabel = new Map();
|
|
565
|
+
|
|
566
|
+
if (options.brainstorm) {
|
|
567
|
+
const storm = brainstormCandidates(rawLabels, options.maxSuggestions);
|
|
568
|
+
rawLabels = storm.labels;
|
|
569
|
+
storyByLabel = storm.storyByLabel;
|
|
570
|
+
console.error(
|
|
571
|
+
`Domainstorm: generated ${rawLabels.length} candidates from seeds [${storm.usedSeeds.join(", ")}]`,
|
|
572
|
+
);
|
|
573
|
+
console.error("Narrative: optimized for agent orchestration, tooling, control plane, and ops positioning.");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const domains = [];
|
|
577
|
+
const seen = new Set();
|
|
578
|
+
|
|
579
|
+
for (const raw of rawLabels) {
|
|
580
|
+
const domain = normalizeDomain(raw, options.tld);
|
|
581
|
+
if (!isValidDomain(domain)) {
|
|
582
|
+
console.error(`Skipping invalid label/domain: ${raw}`);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (!seen.has(domain)) {
|
|
586
|
+
seen.add(domain);
|
|
587
|
+
domains.push(domain);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (domains.length === 0) {
|
|
592
|
+
throw new Error("No valid domains found after normalization.");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const serverLabel = options.server ? `server=${options.server}` : "server=auto";
|
|
596
|
+
console.error(`Checking ${domains.length} domain(s) (${serverLabel}) with concurrency=${options.concurrency}`);
|
|
597
|
+
|
|
598
|
+
const checked = await mapLimit(domains, options.concurrency, async (domain) => {
|
|
599
|
+
const result = await runWhois(domain, options.timeoutMs, options.server);
|
|
600
|
+
const label = domain.slice(0, domain.lastIndexOf("."));
|
|
601
|
+
return {
|
|
602
|
+
...result,
|
|
603
|
+
legalRisk: legalRisk(domain),
|
|
604
|
+
story: storyByLabel.get(label) ?? "",
|
|
605
|
+
};
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const filtered = options.onlyAvailable
|
|
609
|
+
? checked.filter((row) => row.status === "likely_available")
|
|
610
|
+
: checked;
|
|
611
|
+
|
|
612
|
+
for (const row of filtered) {
|
|
613
|
+
let line = `${row.domain}\t${row.status}\t${row.reason}\t${row.legalRisk}`;
|
|
614
|
+
if (row.story) {
|
|
615
|
+
line += `\tstory=${row.story}`;
|
|
616
|
+
}
|
|
617
|
+
if (row.error) {
|
|
618
|
+
line += `\terror=${row.error}`;
|
|
619
|
+
}
|
|
620
|
+
if (options.raw) {
|
|
621
|
+
line += `\t${snippet(row.raw)}`;
|
|
622
|
+
}
|
|
623
|
+
console.log(line);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
await maybeWriteCsv(checked, options.output);
|
|
627
|
+
|
|
628
|
+
const counts = summarize(checked);
|
|
629
|
+
console.error(
|
|
630
|
+
`Summary: likely_available=${counts.likelyAvailable}, registered=${counts.registered}, unknown=${counts.unknown}`,
|
|
631
|
+
);
|
|
632
|
+
if (options.output) {
|
|
633
|
+
console.error(`CSV written: ${path.resolve(options.output)}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
main().catch((err) => {
|
|
638
|
+
console.error(`Error: ${err.message}`);
|
|
639
|
+
printUsage();
|
|
640
|
+
process.exit(1);
|
|
641
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "domainstorm",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Domainstorm CLI: brainstorm brandable names and check domain registration via WHOIS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"domainstorm": "check-md-domains.mjs",
|
|
8
|
+
"domain-check": "check-md-domains.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"check-md-domains.mjs",
|
|
12
|
+
"agent-md-candidates.txt",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node check-md-domains.mjs --help"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"domain",
|
|
20
|
+
"whois",
|
|
21
|
+
"cli",
|
|
22
|
+
"availability"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
}
|
|
28
|
+
}
|