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