@namingsignal/cli 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 +69 -0
- package/dist/core/batch.js +142 -0
- package/dist/core/brief-constraints.js +72 -0
- package/dist/core/brief.js +402 -0
- package/dist/core/candidate-sort.js +47 -0
- package/dist/core/candidates.js +210 -0
- package/dist/core/domain.js +607 -0
- package/dist/core/editorial-review.js +27 -0
- package/dist/core/export.js +235 -0
- package/dist/core/import-safety.js +298 -0
- package/dist/core/index.js +31 -0
- package/dist/core/namesake.js +53 -0
- package/dist/core/namespaces.js +295 -0
- package/dist/core/normalize.js +75 -0
- package/dist/core/scoring.js +409 -0
- package/dist/core/types.js +3 -0
- package/dist/core/url-input.js +28 -0
- package/dist/core/usage.js +271 -0
- package/dist/index.js +642 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zena Labs LLC
|
|
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,69 @@
|
|
|
1
|
+
# @namingsignal/cli
|
|
2
|
+
|
|
3
|
+
CLI for [NamingSignal](https://namingsignal.com): evidence-backed product naming for developers and agents. Local commands are keyless and deterministic; hosted commands call the NamingSignal REST API with your own key.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @namingsignal/cli
|
|
9
|
+
# or run without installing
|
|
10
|
+
npx -y @namingsignal/cli help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node 20 or newer.
|
|
14
|
+
|
|
15
|
+
## Keyless local commands
|
|
16
|
+
|
|
17
|
+
No account, no API key, no telemetry. Domain checks talk directly to public RDAP registries from your machine.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Score names and run live registry checks
|
|
21
|
+
namingsignal check offhook nicless --tlds com,app --namespaces --json
|
|
22
|
+
|
|
23
|
+
# Compile a privacy-bounded naming brief from a repository or file
|
|
24
|
+
namingsignal brief /path/to/project --dry-run
|
|
25
|
+
namingsignal brief /path/to/project --out naming-brief.json
|
|
26
|
+
|
|
27
|
+
# Deterministic candidate generation from an approved brief (degraded-mode starting set)
|
|
28
|
+
namingsignal generate --brief naming-brief.json --count 40
|
|
29
|
+
|
|
30
|
+
# Portable decision record from prior evidence
|
|
31
|
+
namingsignal compare offhook quitcore --brief naming-brief.json --evidence prior-check.json --format md
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The `brief` scanner is allowlisted and bounded: it reads README/agent instructions/manifests/docs text only, excludes source code, `.env` files, credentials, lockfiles, and build output, and `--dry-run` previews the exact file selection before anything is compiled.
|
|
35
|
+
|
|
36
|
+
## Hosted commands
|
|
37
|
+
|
|
38
|
+
`research` and `health` call the hosted API (default `https://namingsignal.com`). Create a free key at https://namingsignal.com/developers/keys and export it; never commit it.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export NAMING_SIGNAL_API_KEY=your-user-key
|
|
42
|
+
|
|
43
|
+
namingsignal research offhook quitcore nicless --brief naming-brief.json
|
|
44
|
+
namingsignal health
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`--api <url>` or `NAMING_SIGNAL_API_URL` overrides the API origin (for example `http://localhost:3000` for a local development server).
|
|
48
|
+
|
|
49
|
+
## JSON mode
|
|
50
|
+
|
|
51
|
+
Machine-readable JSON is written to stdout whenever `--json` is passed or stdout is not a TTY, so agent pipelines get structured output by default. Diagnostics go to stderr.
|
|
52
|
+
|
|
53
|
+
## Exit codes
|
|
54
|
+
|
|
55
|
+
| Code | Meaning |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| 0 | Success |
|
|
58
|
+
| 1 | Unexpected or network error |
|
|
59
|
+
| 2 | Usage error |
|
|
60
|
+
| 3 | Auth error (401/403) |
|
|
61
|
+
| 4 | Quota or rate limit (402/429) |
|
|
62
|
+
|
|
63
|
+
## Evidence honesty
|
|
64
|
+
|
|
65
|
+
RDAP not-found means likely unregistered at check time, not a purchase guarantee. Timeouts, rate limits, and unsupported TLDs stay unknown and are never reported as available. Naming research is preliminary; it is not trademark clearance or legal advice.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT, Copyright (c) 2026 Zena Labs LLC.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.chunkItems = chunkItems;
|
|
4
|
+
exports.classifyCandidate = classifyCandidate;
|
|
5
|
+
exports.bestQualifyingDomain = bestQualifyingDomain;
|
|
6
|
+
exports.sortCandidates = sortCandidates;
|
|
7
|
+
const TAKEN_DOMAIN_STATES = new Set([
|
|
8
|
+
"registered",
|
|
9
|
+
"premium",
|
|
10
|
+
"parked",
|
|
11
|
+
"active",
|
|
12
|
+
]);
|
|
13
|
+
function normalizeTld(value) {
|
|
14
|
+
const normalized = value.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
|
|
15
|
+
return /^[a-z0-9-]+$/.test(normalized) ? normalized : "";
|
|
16
|
+
}
|
|
17
|
+
function selectedTlds(values) {
|
|
18
|
+
const result = [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const value of values) {
|
|
21
|
+
const tld = normalizeTld(value);
|
|
22
|
+
if (!tld || seen.has(tld))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(tld);
|
|
25
|
+
result.push(tld);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function domainTld(domain) {
|
|
30
|
+
const labels = domain.trim().toLowerCase().replace(/\.$/, "").split(".");
|
|
31
|
+
return labels.length > 1 ? labels.at(-1) ?? "" : "";
|
|
32
|
+
}
|
|
33
|
+
function stateForTld(domains, tld) {
|
|
34
|
+
const states = new Set();
|
|
35
|
+
for (const domain of domains) {
|
|
36
|
+
if (domainTld(domain.domain) !== tld)
|
|
37
|
+
continue;
|
|
38
|
+
if (domain.status === "likely_available")
|
|
39
|
+
states.add("available");
|
|
40
|
+
else if (TAKEN_DOMAIN_STATES.has(domain.status))
|
|
41
|
+
states.add("taken");
|
|
42
|
+
else
|
|
43
|
+
states.add("review");
|
|
44
|
+
}
|
|
45
|
+
// Missing evidence and conflicting duplicate evidence both require review.
|
|
46
|
+
if (states.size !== 1)
|
|
47
|
+
return "review";
|
|
48
|
+
return states.values().next().value ?? "review";
|
|
49
|
+
}
|
|
50
|
+
function chunkItems(items, chunkSize) {
|
|
51
|
+
if (!Number.isSafeInteger(chunkSize) || chunkSize < 1) {
|
|
52
|
+
throw new RangeError("Chunk size must be a positive safe integer.");
|
|
53
|
+
}
|
|
54
|
+
const chunks = [];
|
|
55
|
+
for (let index = 0; index < items.length; index += chunkSize) {
|
|
56
|
+
chunks.push(items.slice(index, index + chunkSize));
|
|
57
|
+
}
|
|
58
|
+
return chunks;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Classify only against the selected TLDs. Unknown, unsupported, missing, or
|
|
62
|
+
* conflicting evidence never becomes available or taken by implication.
|
|
63
|
+
*/
|
|
64
|
+
function classifyCandidate(candidate, tlds, rule) {
|
|
65
|
+
const normalizedTlds = selectedTlds(tlds);
|
|
66
|
+
if (!normalizedTlds.length)
|
|
67
|
+
return "review";
|
|
68
|
+
const states = normalizedTlds.map((tld) => stateForTld(candidate.domains ?? [], tld));
|
|
69
|
+
if (rule === "any_selected") {
|
|
70
|
+
if (states.includes("available"))
|
|
71
|
+
return "available";
|
|
72
|
+
return states.every((state) => state === "taken") ? "taken" : "review";
|
|
73
|
+
}
|
|
74
|
+
if (rule === "all_selected") {
|
|
75
|
+
if (states.every((state) => state === "available"))
|
|
76
|
+
return "available";
|
|
77
|
+
return states.includes("taken") ? "taken" : "review";
|
|
78
|
+
}
|
|
79
|
+
throw new TypeError(`Unsupported availability rule: ${String(rule)}`);
|
|
80
|
+
}
|
|
81
|
+
/** Return the highest-priority selected domain that satisfies the rule. */
|
|
82
|
+
function bestQualifyingDomain(candidate, tlds, rule = "any_selected") {
|
|
83
|
+
if (classifyCandidate(candidate, tlds, rule) !== "available")
|
|
84
|
+
return undefined;
|
|
85
|
+
const domains = candidate.domains ?? [];
|
|
86
|
+
for (const tld of selectedTlds(tlds)) {
|
|
87
|
+
const result = domains.find((domain) => domainTld(domain.domain) === tld && domain.status === "likely_available");
|
|
88
|
+
if (result)
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function finiteScore(value) {
|
|
94
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
95
|
+
}
|
|
96
|
+
function scoreForKey(score, key) {
|
|
97
|
+
if (!score)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (key === "overall")
|
|
100
|
+
return finiteScore(score.overall);
|
|
101
|
+
return finiteScore(score.dimensions.find((dimension) => dimension.key === key)?.score);
|
|
102
|
+
}
|
|
103
|
+
function compareScoresDescending(left, right) {
|
|
104
|
+
if (left === undefined && right === undefined)
|
|
105
|
+
return 0;
|
|
106
|
+
if (left === undefined)
|
|
107
|
+
return 1;
|
|
108
|
+
if (right === undefined)
|
|
109
|
+
return -1;
|
|
110
|
+
return right - left;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Return a new deterministic ranking. The selected score descends first,
|
|
114
|
+
* followed by overall score, display name, and original order as the final
|
|
115
|
+
* stable tie-break.
|
|
116
|
+
*/
|
|
117
|
+
function sortCandidates(candidates, key = "overall") {
|
|
118
|
+
return candidates
|
|
119
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
120
|
+
.sort((left, right) => {
|
|
121
|
+
const primary = compareScoresDescending(scoreForKey(left.candidate.score, key), scoreForKey(right.candidate.score, key));
|
|
122
|
+
if (primary)
|
|
123
|
+
return primary;
|
|
124
|
+
if (key !== "overall") {
|
|
125
|
+
const overall = compareScoresDescending(finiteScore(left.candidate.score?.overall), finiteScore(right.candidate.score?.overall));
|
|
126
|
+
if (overall)
|
|
127
|
+
return overall;
|
|
128
|
+
}
|
|
129
|
+
const insensitiveName = left.candidate.name.localeCompare(right.candidate.name, "en", {
|
|
130
|
+
numeric: true,
|
|
131
|
+
sensitivity: "base",
|
|
132
|
+
});
|
|
133
|
+
if (insensitiveName)
|
|
134
|
+
return insensitiveName;
|
|
135
|
+
const exactName = left.candidate.name.localeCompare(right.candidate.name, "en", {
|
|
136
|
+
numeric: true,
|
|
137
|
+
sensitivity: "variant",
|
|
138
|
+
});
|
|
139
|
+
return exactName || left.index - right.index;
|
|
140
|
+
})
|
|
141
|
+
.map(({ candidate }) => candidate);
|
|
142
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.explicitAvoidTerms = explicitAvoidTerms;
|
|
4
|
+
exports.candidateAvoidViolations = candidateAvoidViolations;
|
|
5
|
+
exports.candidateBriefConflicts = candidateBriefConflicts;
|
|
6
|
+
const normalize_1 = require("./normalize");
|
|
7
|
+
function explicitAvoidTerms(brief) {
|
|
8
|
+
if (!brief)
|
|
9
|
+
return [];
|
|
10
|
+
const terms = new Set();
|
|
11
|
+
for (const entry of brief.avoid) {
|
|
12
|
+
const normalized = (0, normalize_1.normalizeName)(entry);
|
|
13
|
+
const words = normalized.split(" ").filter(Boolean);
|
|
14
|
+
if (words.length <= 2 && normalized.length <= 28)
|
|
15
|
+
terms.add((0, normalize_1.comparisonKey)(normalized));
|
|
16
|
+
const examples = entry.match(/(?:such as|including|brands?\s*:?)\s+(.+)$/i)?.[1];
|
|
17
|
+
if (examples) {
|
|
18
|
+
for (const example of examples.split(/[,;/]|\band\b/i)) {
|
|
19
|
+
const key = (0, normalize_1.comparisonKey)(example);
|
|
20
|
+
if (key.length >= 3 && key.length <= 24)
|
|
21
|
+
terms.add(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [...terms].filter((term) => term.length >= 3);
|
|
26
|
+
}
|
|
27
|
+
function candidateAvoidViolations(name, brief) {
|
|
28
|
+
const key = (0, normalize_1.comparisonKey)(name);
|
|
29
|
+
return explicitAvoidTerms(brief).filter((term) => key.includes(term));
|
|
30
|
+
}
|
|
31
|
+
function candidateBriefConflicts(name, brief) {
|
|
32
|
+
if (!brief)
|
|
33
|
+
return { blockers: [], warnings: [], penalty: 0 };
|
|
34
|
+
const key = (0, normalize_1.comparisonKey)(name);
|
|
35
|
+
const context = (0, normalize_1.normalizeName)([
|
|
36
|
+
brief.product,
|
|
37
|
+
brief.pain,
|
|
38
|
+
brief.outcome,
|
|
39
|
+
brief.mechanism,
|
|
40
|
+
brief.futureExpansion,
|
|
41
|
+
...brief.avoid,
|
|
42
|
+
].join(" "));
|
|
43
|
+
const blockers = [];
|
|
44
|
+
const warnings = [];
|
|
45
|
+
let penalty = 0;
|
|
46
|
+
const avoided = candidateAvoidViolations(name, brief);
|
|
47
|
+
if (avoided.length) {
|
|
48
|
+
blockers.push(`${name} contains an explicitly avoided term: ${avoided.join(", ")}.`);
|
|
49
|
+
}
|
|
50
|
+
if (/\b(?:audio|audible|voice|announces|calls every rep|podcast|music)\b/.test(context) && /(?:inaudible|voiceless|muted)/.test(key)) {
|
|
51
|
+
blockers.push(`${name} contradicts the brief's audible-guidance mechanism.`);
|
|
52
|
+
}
|
|
53
|
+
if (/\b(?:finish|completion|purpose|useful|guided|carries the workout)\b/.test(context) && /(?:aimless|purposeless)/.test(key)) {
|
|
54
|
+
blockers.push(`${name} contradicts the brief's purposeful-completion outcome.`);
|
|
55
|
+
}
|
|
56
|
+
if (/\b(?:avoid|not aggressive|gym bro|punishment|negative)\b/.test(context) && /(?:mindless|gainz|shred|beast|crush|punish)/.test(key)) {
|
|
57
|
+
blockers.push(`${name} uses the negative or gym-bro language the brief excludes.`);
|
|
58
|
+
}
|
|
59
|
+
if (/\b(?:relief|without managing|low decision|mental overhead|autopilot)\b/.test(context) && /(?:tethered|trapped|bound)/.test(key)) {
|
|
60
|
+
warnings.push(`${name} implies restriction while the brief promises relief from micromanagement.`);
|
|
61
|
+
penalty += 18;
|
|
62
|
+
}
|
|
63
|
+
if (/\b(?:workout|exercise|reps|sets)\b/.test(context) && !/\b(?:cycling|bike|bicycle|ride)\b/.test(context) && /(?:pedal|bicycle|cycling)/.test(key)) {
|
|
64
|
+
warnings.push(`${name} suggests cycling even though the brief describes a broader workout product.`);
|
|
65
|
+
penalty += 22;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
blockers: [...new Set(blockers)],
|
|
69
|
+
warnings: [...new Set(warnings)],
|
|
70
|
+
penalty: Math.min(35, penalty),
|
|
71
|
+
};
|
|
72
|
+
}
|