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 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
+ }