Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
"url": "https://profullstack.com"
},
"homepage": "https://github.com/profullstack/cli-tools#install",
"keywords": ["cli", "install", "path", "aliases", "moshcode"]
"keywords": [
"cli",
"install",
"path",
"aliases",
"moshcode"
]
},
{
"name": "blog",
Expand All @@ -39,7 +45,7 @@
},
{
"name": "domain",
"description": "Find domains you can actually register, and look one up in depth. Availability is read from the registry over RDAP, never guessed from DNS, so parked names and registrations with no nameservers are not mistaken for free.",
"description": "Generate candidate product names from a sentence, find the ones you can actually register, and look one up in depth. Availability is read from the registry over RDAP, never guessed from DNS, so parked names and registrations with no nameservers are not mistaken for free.",
"source": "./plugins/domain",
"category": "productivity",
"author": {
Expand All @@ -53,7 +59,8 @@
"dns",
"whois",
"availability",
"naming"
"naming",
"llm"
]
}
]
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,39 @@ The scanner itself lives in the threatcrush checkout, so this is a launcher.
Point it elsewhere with `TCFEED_REPO`; every other `TCFEED_*` variable is read
by the script it launches and works unchanged.

### `generate-names`

Turn a sentence describing a product into a long list of candidate names, ready
to pipe into `domainfree`.

```sh
generate-names "a registry that checks whether Lean proofs actually compile"
generate-names "a tool that finds dead states in agent graphs" -n 1000 --tld dev
generate-names "an open directory of independent blogs" | domainfree
```

**It asks the model for vocabulary, not for a thousand names.** One cheap call
returns ~40 head words and ~40 modifiers; the cross product is expanded locally
and shuffled. Asking a model for 1,000 names directly repeats itself within a
few hundred, drifts off-brief, and costs far more — and the call count here is
the same whether you ask for 10 names or 10,000.

Needs `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. Whichever is set is used;
OpenAI wins if both are. Defaults are the cheap tier on each side
(`gpt-4.1-mini` / `claude-haiku-4-5`) and are overridable with `--model`.

| Flag | Effect |
| --- | --- |
| `-n, --count N` | how many names to print, default 1000 |
| `--tld TLD` | extension to append, default `com` |
| `--words N` | 1 or 2 English words per name, default 2 |
| `--provider P` | `openai` or `anthropic`, default whichever key is set |
| `--model M` | override the model |
| `--seed N` | shuffle seed; the same seed reproduces the same list |
| `--timeout MS` | API timeout, default 60000 |

Names go to stdout and the summary to stderr, so the output pipes cleanly.

### `domainfree`

Bulk domain availability, straight from the registry. Prints only the names you
Expand All @@ -192,7 +225,8 @@ can actually buy, one per line, so it pipes into anything.
```sh
domainfree sorrycheck.com sinkstate.com
domainfree --file candidates.txt
generate-names | domainfree --jobs 24
generate-names "a registry that checks Lean proofs" | domainfree --jobs 24
printf '%s\n' sorry{check,lint,scan}.com | domainfree
domainfree --all example.com # show TAKEN rows too
```

Expand Down
3 changes: 2 additions & 1 deletion bin/domainfree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
const USAGE = `Usage:
domainfree <name>...
domainfree --file candidates.txt
generate-names | domainfree --jobs 24
generate-names "a registry that checks Lean proofs" | domainfree
printf '%s\n' sorry{check,lint,scan}.com | domainfree

Availability is read from RDAP, never inferred from DNS: a parked domain
resolves but is taken, and a domain registered with no nameservers returns
Expand Down
105 changes: 105 additions & 0 deletions bin/generate-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env -S npx --yes tsx
/**
* generate-names — turn a sentence into a long list of candidate names.
*
* Pipes straight into `domainfree`, which is the point:
*
* generate-names "a registry that checks Lean proofs" | domainfree
*/

import { UsageError, integer, parseArgs } from '../src/args.ts';
import { isMain } from '../src/is-main.ts';
import {
DEFAULT_COUNT,
DEFAULT_MODELS,
DEFAULT_TLD,
anthropicCaller,
generateNames,
openaiCaller,
resolveProvider,
} from '../src/generate-names.ts';

const USAGE = `Usage:
generate-names "<what the product does>"
generate-names "a registry that checks Lean proofs" | domainfree

Asks a cheap model for naming vocabulary, then expands it locally into
candidates. One small API call regardless of --count: asking a model for a
thousand names directly repeats itself and costs far more.

Options:
-n, --count N how many names to print (default: ${DEFAULT_COUNT})
--tld TLD extension to append (default: ${DEFAULT_TLD})
--words N 1 or 2 English words per name (default: 2)
--provider P openai | anthropic (default: whichever key is set)
--model M override the model (default: ${DEFAULT_MODELS.openai} / ${DEFAULT_MODELS.anthropic})
--seed N shuffle seed; the same seed reproduces the same list
--timeout MS API timeout (default: 60000)
-h, --help show this help

Needs OPENAI_API_KEY or ANTHROPIC_API_KEY. Names go to stdout and nothing
else does, so the output pipes cleanly.
`;

if (isMain(import.meta.url)) {
try {
const { flags, values, positional } = parseArgs(process.argv.slice(2), {
boolean: ['-h', '--help'],
string: [
'-n', '--count', '--tld', '--words', '--provider', '--model', '--seed', '--timeout',
],
});

if (flags.has('-h') || flags.has('--help') || positional.length === 0) {
process.stdout.write(USAGE);
process.exit(positional.length === 0 && !flags.has('-h') && !flags.has('--help') ? 1 : 0);
}

const description = positional.join(' ').trim();
if (description.length < 8) {
throw new UsageError('describe the product in a sentence, not a word');
}

const count = integer(values, values.has('-n') ? '-n' : '--count', DEFAULT_COUNT, {
min: 1,
max: 20_000,
});
const seed = integer(values, '--seed', 1, { min: 0, max: 2 ** 31 });
const timeout = integer(values, '--timeout', 60_000, { min: 1000, max: 600_000 });
const wordCount = integer(values, '--words', 2, { min: 1, max: 2 }) as 1 | 2;
const tld = (values.get('--tld') ?? DEFAULT_TLD).replace(/^\./, '');
if (!/^[a-z]{2,}$/i.test(tld)) throw new UsageError(`--tld must be letters, got "${tld}"`);

let provider;
try {
provider = resolveProvider(process.env, values.get('--provider'));
} catch (error) {
// A bad --provider is a typo and a missing key is a setup problem; both
// are the caller's to fix, so report them like any other usage error.
throw new UsageError(error instanceof Error ? error.message : String(error));
}
const model = values.get('--model') ?? DEFAULT_MODELS[provider];
const apiKey = process.env[provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY']!;
const call =
provider === 'openai'
? openaiCaller(apiKey, model, timeout)
: anthropicCaller(apiKey, model, timeout);

const names = await generateNames(description, call, {
count,
tld,
seed,
words: wordCount,
});

for (const name of names) process.stdout.write(`${name}\n`);
process.stderr.write(`${names.length} names · ${provider}/${model}\n`);
} catch (error) {
if (error instanceof UsageError) {
process.stderr.write(`generate-names: ${error.message}\n`);
process.exit(1);
}
process.stderr.write(`generate-names: ${error instanceof Error ? error.message : error}\n`);
process.exit(2);
}
}
12 changes: 10 additions & 2 deletions plugins/domain/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
{
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
"name": "domain",
"description": "Find domains you can actually register, and look one up in depth. Availability is read from the registry over RDAP, never guessed from DNS, so parked names and registrations with no nameservers are not mistaken for free.",
"description": "Generate candidate product names from a sentence, find the ones you can actually register, and look one up in depth. Availability is read from the registry over RDAP, never guessed from DNS, so parked names and registrations with no nameservers are not mistaken for free.",
"version": "0.1.0",
"author": {
"name": "profullstack",
"url": "https://profullstack.com"
},
"homepage": "https://github.com/profullstack/cli-tools#domainfree",
"license": "MIT",
"keywords": ["domain", "rdap", "dns", "whois", "availability", "naming"]
"keywords": [
"domain",
"rdap",
"dns",
"whois",
"availability",
"naming",
"llm"
]
}
1 change: 1 addition & 0 deletions plugins/domain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ than guessing from DNS.

| Command | Does |
| --- | --- |
| `/domain:names` | Turn a sentence about your product into a thousand candidates. |
| `/domain:free` | Filter a list of names down to the ones you can actually register. |
| `/domain:lookup` | Everything about one name — RDAP record, dates, nameservers, DNS, reverse PTR — as JSON. |

Expand Down
53 changes: 53 additions & 0 deletions plugins/domain/commands/names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
description: Turn a sentence about your product into a thousand candidate names.
allowed-tools: Bash(generate-names:*), Bash(domainfree:*), Write
---

## Task

Generate candidate names from a description, then keep only the ones that can
actually be registered.

```bash
generate-names "a registry that checks whether Lean proofs actually compile" | domainfree
```

Save the survivors when there are many:

```bash
generate-names "a tool that finds dead states in agent graphs" -n 1000 \
| domainfree > free.txt
```

## How it works, and why it matters

The model is asked for **vocabulary**, not for a thousand names: roughly 40
head words and 40 modifiers, expanded locally into the cross product and
shuffled. That is one cheap API call whether you want 10 names or 10,000.

Asking a model for a thousand names directly is the obvious approach and the
wrong one — it repeats itself within a few hundred, drifts off the brief, and
costs far more for a worse list.

## Choosing well from the output

The generator is deliberately high-volume and low-precision; `domainfree` is
the filter, and you are the judge. Expect most combinations to be noise and a
handful to be good. Worth weighing:

- Does the name say what the thing does, or only gesture at it?
- Would the intended audience recognise the vocabulary? Insider terms are an
asset with practitioners and a wall with everyone else.
- Read it aloud. If it needs spelling out, it will need spelling out forever.

## Options worth knowing

- `-n 1000` — how many to print. Default is already 1000.
- `--tld dev` — any extension, not just `.com`.
- `--words 1` — single-word names instead of two-word compounds.
- `--seed 42` — the same seed reproduces the same list from the same vocabulary.
- `--provider anthropic` — force a provider; by default it uses whichever of
`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` is set.

Availability is checked by `/domain:free`, which reads the registry over RDAP
rather than guessing from DNS. Re-check immediately before buying.
Loading
Loading