claudemd-cli 0.68.4 → 0.69.1
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/README.md +21 -2
- package/bin/claudemd-lint.js +5 -36
- package/package.json +1 -1
- package/scripts/lib/argv.js +126 -0
- package/scripts/lib/lint.js +12 -6
- package/CHANGELOG.md +0 -5013
package/README.md
CHANGED
|
@@ -215,6 +215,19 @@ export DISABLE_BOOTSTRAP_FAIL_BANNER=1 # v0.50.0+ — only the SessionStart
|
|
|
215
215
|
# sentinel + bootstrap.log trail keep being
|
|
216
216
|
# written so the state stays diagnosable.
|
|
217
217
|
|
|
218
|
+
export DISABLE_SPEC_DRIFT_BANNER=1 # only the SessionStart banner reporting
|
|
219
|
+
# installed-spec vs plugin-spec drift; the
|
|
220
|
+
# version check itself still runs, so
|
|
221
|
+
# /claudemd-doctor keeps reporting the drift.
|
|
222
|
+
|
|
223
|
+
export DISABLE_RULE_HITS_LOG=1 # stop every hook from appending to
|
|
224
|
+
# ~/.claude/logs/claudemd.jsonl. Enforcement
|
|
225
|
+
# is unaffected (a deny still denies); what
|
|
226
|
+
# stops is the telemetry /claudemd-audit,
|
|
227
|
+
# /claudemd-rules and the §13.1 demote loop
|
|
228
|
+
# read. Set it while hand-probing a hook so
|
|
229
|
+
# the probe does not land in the real corpus.
|
|
230
|
+
|
|
218
231
|
export DISABLE_BATCH_CADENCE_ADVISORY=1 # v0.19.2+ — only the §13.2 batch-review
|
|
219
232
|
# cadence advisory inside session-end-check;
|
|
220
233
|
# mid-SPINE warn-on-unvalidated-mutation
|
|
@@ -353,7 +366,7 @@ claudemd/
|
|
|
353
366
|
├── .claude-plugin/
|
|
354
367
|
│ ├── plugin.json # minimal manifest (name, version, author, license, keywords)
|
|
355
368
|
│ └── marketplace.json # marketplace catalog entry
|
|
356
|
-
├── hooks/ # 15 shell hooks + hooks/lib/ (hook-common, rule-hits, platform)
|
|
369
|
+
├── hooks/ # 15 shell hooks + hooks/lib/ (hook-common, rule-hits, platform, memory-tags)
|
|
357
370
|
│ └── hooks.json # authoritative hook registration (v0.1.5+); CC expands ${CLAUDE_PLUGIN_ROOT} here
|
|
358
371
|
├── commands/ # 16 slash-command markdown files
|
|
359
372
|
├── bin/ # standalone CLI entrypoint (claudemd-lint.js → `npx claudemd-cli` on npmjs.org)
|
|
@@ -368,12 +381,18 @@ claudemd/
|
|
|
368
381
|
|
|
369
382
|
## Extending
|
|
370
383
|
|
|
371
|
-
- **Add a new hook** — see [`docs/ADDING-NEW-HOOK.md`](docs/ADDING-NEW-HOOK.md) for the
|
|
384
|
+
- **Add a new hook** — see [`docs/ADDING-NEW-HOOK.md`](docs/ADDING-NEW-HOOK.md) for the full checklist (hook script + test + plugin registration + telemetry gates + docs + the content gates a new hook trips + version bump).
|
|
372
385
|
- **Rule-hits log schema** — [`docs/RULE-HITS-SCHEMA.md`](docs/RULE-HITS-SCHEMA.md) for the JSONL row format used by `/claudemd-audit`.
|
|
373
386
|
- **Design rationale + decisions log** — [`docs/superpowers/specs/2026-04-21-claudemd-plugin-design.md`](docs/superpowers/specs/2026-04-21-claudemd-plugin-design.md).
|
|
374
387
|
|
|
375
388
|
---
|
|
376
389
|
|
|
390
|
+
## Changelog
|
|
391
|
+
|
|
392
|
+
Every release is recorded in [`CHANGELOG.md`](https://github.com/sdsrss/claudemd/blob/main/CHANGELOG.md) on GitHub. It is not shipped in the npm tarball — at 727 KB it was 89% of a package whose runtime is 95 KB unpacked (35 KB compressed), and `npx claudemd-cli` re-downloads that tarball on every cold run.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
377
396
|
## License
|
|
378
397
|
|
|
379
398
|
MIT — see [LICENSE](LICENSE).
|
package/bin/claudemd-lint.js
CHANGED
|
@@ -29,6 +29,11 @@ import {
|
|
|
29
29
|
stripGitCommitComments,
|
|
30
30
|
looksLikeGitMessageFile,
|
|
31
31
|
} from '../scripts/lib/lint.js';
|
|
32
|
+
// Shared argv authority. It used to be a private function here, which is how
|
|
33
|
+
// scripts/lint-argv.js came to authenticate a CLI's argv contract by FUNCTION
|
|
34
|
+
// NAME — any file declaring a local `validateAndExpandFlags` satisfied the gate
|
|
35
|
+
// (audit-2026-08-22 条目 14). The gate now requires this import.
|
|
36
|
+
import { validateAndExpandFlags } from '../scripts/lib/argv.js';
|
|
32
37
|
|
|
33
38
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
34
39
|
const REPO_ROOT = path.resolve(HERE, '..');
|
|
@@ -142,42 +147,6 @@ function workTreeOf(git, cwd) {
|
|
|
142
147
|
return cwd;
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
// Strict-validate flag-shaped args + normalize `--key=value` → `--key value`
|
|
146
|
-
// pairs so the existing space-form parsing below works on either shape.
|
|
147
|
-
// Catches the same antipattern the slash-command CLIs hit in v0.9.16/0.9.17:
|
|
148
|
-
// `args.includes('--json')` returns false for `--json=yes`, so the flag was
|
|
149
|
-
// silently dropped; `args.indexOf('--file')` returns -1 for `--file=PATH`,
|
|
150
|
-
// so the value was silently ignored; an unknown `--jzon` typo was silently
|
|
151
|
-
// stripped from positional and never surfaced. Each path now exits 2.
|
|
152
|
-
function validateAndExpandFlags(args, knownBools, knownValues, sub) {
|
|
153
|
-
const out = [];
|
|
154
|
-
const bools = new Set(knownBools);
|
|
155
|
-
const values = new Set(knownValues);
|
|
156
|
-
for (const a of args) {
|
|
157
|
-
if (!a.startsWith('--')) { out.push(a); continue; }
|
|
158
|
-
if (a.includes('=')) {
|
|
159
|
-
const eq = a.indexOf('=');
|
|
160
|
-
const k = a.slice(0, eq);
|
|
161
|
-
const v = a.slice(eq + 1);
|
|
162
|
-
if (bools.has(k)) {
|
|
163
|
-
process.stderr.write(`${sub}: '${k}' is a boolean flag and does not take a value (got '${a}'). Drop the '=...' suffix.\n`);
|
|
164
|
-
process.exit(2);
|
|
165
|
-
}
|
|
166
|
-
if (values.has(k)) {
|
|
167
|
-
out.push(k);
|
|
168
|
-
out.push(v);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
process.stderr.write(`${sub}: unknown flag '${k}' (got '${a}').\n`);
|
|
172
|
-
process.exit(2);
|
|
173
|
-
}
|
|
174
|
-
if (bools.has(a) || values.has(a)) { out.push(a); continue; }
|
|
175
|
-
process.stderr.write(`${sub}: unknown flag '${a}'.\n`);
|
|
176
|
-
process.exit(2);
|
|
177
|
-
}
|
|
178
|
-
return out;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
150
|
function lintCmd(rawArgs) {
|
|
182
151
|
const args = validateAndExpandFlags(
|
|
183
152
|
rawArgs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudemd-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.1",
|
|
4
4
|
"description": "Standalone CLI for §10-V banned-vocab + transcript scanning. Companion to the claudemd Claude Code plugin (github.com/sdsrss/claudemd) for use in git pre-commit hooks, GitHub Actions, and other agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Strict argv parser for slash-command CLI scripts (clean-residue / audit /
|
|
2
|
+
// sparkline). Three contracts the previous inline parsers silently violated:
|
|
3
|
+
// 1. `--key=value` is the ONLY accepted shape for value flags. The space form
|
|
4
|
+
// `--key value` falls back to default + ignores the value, exiting 0 — same
|
|
5
|
+
// footgun family as the v0.9.14 `claudemd-cli lint <path>` silent-success.
|
|
6
|
+
// 2. Unknown flags reject loudly. Pre-fix, every script's `args.find()` lookup
|
|
7
|
+
// silently dropped anything it didn't recognize, so a typo produced
|
|
8
|
+
// indistinguishable-from-success output.
|
|
9
|
+
// 3. `--key=value` for boolean flags rejects (`--apply=yes` shouldn't parse
|
|
10
|
+
// as `--apply` true).
|
|
11
|
+
// Caller catches `ArgvError` and exits 2 (distinct from numeric-validation
|
|
12
|
+
// exit 1) so wrappers can tell parsing-shape errors from validation errors.
|
|
13
|
+
|
|
14
|
+
export class ArgvError extends Error {
|
|
15
|
+
constructor(message) { super(message); this.name = 'ArgvError'; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Discoverability helper: when `--help` or `-h` is the first non-empty arg
|
|
19
|
+
// (or anywhere in argv for scripts with no flags), print usage to stdout and
|
|
20
|
+
// exit 0. Caller invokes BEFORE parseStrict so unknown-arg rejection doesn't
|
|
21
|
+
// shadow the universal first-probe of every Unix CLI. Pre-fix, every
|
|
22
|
+
// parseStrict-using script (audit / sparkline / hard-rules-audit /
|
|
23
|
+
// clean-residue / doctor) responded `Unknown argument: '--help'.` exit 2 —
|
|
24
|
+
// classic discoverability bug for new users.
|
|
25
|
+
export function printHelpAndExit(argv, usage) {
|
|
26
|
+
if (argv.some(a => a === '--help' || a === '-h')) {
|
|
27
|
+
process.stdout.write(usage.endsWith('\n') ? usage : usage + '\n');
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Strict positive-integer validator for numeric flags (`--days` / `--age-days`
|
|
33
|
+
// / `--prune-backups` / `--sample`). Returns the integer when `raw` (after
|
|
34
|
+
// trimming surrounding whitespace) is a plain base-10 positive integer with no
|
|
35
|
+
// leading zero, else null. `Number()` alone is too permissive — it coerces
|
|
36
|
+
// '0x1e' → 30, '1e2' → 100, ' 30 ' → 30 — all of which pass `Number.isInteger`
|
|
37
|
+
// despite help text promising a "positive integer", a silent contract
|
|
38
|
+
// divergence (inverse of the `parseInt` truncation footgun). Mirrors the
|
|
39
|
+
// `/^[1-9][0-9]*$/` guard already used for CLAUDEMD_BATCH_THRESHOLD in status.js.
|
|
40
|
+
export function parsePositiveInt(raw) {
|
|
41
|
+
if (raw == null) return null;
|
|
42
|
+
const s = String(raw).trim();
|
|
43
|
+
// Plain base-10 notation only — rejects hex ('0x1e'), exponential ('1e2'),
|
|
44
|
+
// signs, and interior junk that `Number()` would coerce. A trailing-zero
|
|
45
|
+
// decimal ('30.0', '30.00') is allowed through the shape gate so the
|
|
46
|
+
// integer-valued-float check below can accept it (existing contract: callers
|
|
47
|
+
// / scripts may pass '30.0'); a true fraction ('1.5') passes the shape gate
|
|
48
|
+
// but fails Number.isInteger and is rejected.
|
|
49
|
+
if (!/^[0-9]+(\.[0-9]+)?$/.test(s)) return null;
|
|
50
|
+
const n = Number(s);
|
|
51
|
+
if (!Number.isInteger(n) || n < 1) return null;
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseStrict(argv, { bools = [], values = [] } = {}) {
|
|
56
|
+
const out = { bools: new Set(), values: {} };
|
|
57
|
+
const knownBool = new Set(bools);
|
|
58
|
+
const knownValue = new Set(values);
|
|
59
|
+
for (const a of argv) {
|
|
60
|
+
if (knownBool.has(a)) { out.bools.add(a); continue; }
|
|
61
|
+
if (a.startsWith('--') && a.includes('=')) {
|
|
62
|
+
const eq = a.indexOf('=');
|
|
63
|
+
const k = a.slice(0, eq);
|
|
64
|
+
const v = a.slice(eq + 1);
|
|
65
|
+
if (knownValue.has(k)) { out.values[k] = v; continue; }
|
|
66
|
+
if (knownBool.has(k)) {
|
|
67
|
+
throw new ArgvError(`Boolean flag '${k}' does not take a value (got '${a}').`);
|
|
68
|
+
}
|
|
69
|
+
throw new ArgvError(`Unknown flag: '${k}'.`);
|
|
70
|
+
}
|
|
71
|
+
if (knownValue.has(a)) {
|
|
72
|
+
throw new ArgvError(`'${a}' requires '=value' form (got '${a}' bare). Use '${a}=N'.`);
|
|
73
|
+
}
|
|
74
|
+
throw new ArgvError(`Unknown argument: '${a}'.`);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Space-form argv validator for the published `claudemd-cli` binary.
|
|
80
|
+
//
|
|
81
|
+
// Lived in bin/claudemd-lint.js until audit-2026-08-22 条目 14: a second argv
|
|
82
|
+
// authority beside parseStrict, and scripts/lint-argv.js authenticated it BY
|
|
83
|
+
// FUNCTION NAME, so any file that declared a local function called
|
|
84
|
+
// `validateAndExpandFlags` satisfied the gate without validating anything. The
|
|
85
|
+
// gate had been widened to accommodate the duplicate instead of the duplicate
|
|
86
|
+
// being converged. It is not merged into parseStrict because the two contracts
|
|
87
|
+
// genuinely differ and the difference is published: parseStrict rejects the
|
|
88
|
+
// `--key value` space form and positional arguments, both of which
|
|
89
|
+
// `claudemd-cli lint <path> --comment-char ';'` documents and users' pre-commit
|
|
90
|
+
// hooks depend on.
|
|
91
|
+
//
|
|
92
|
+
// Strict-validate flag-shaped args + normalize `--key=value` → `--key value`
|
|
93
|
+
// pairs so the existing space-form parsing below works on either shape.
|
|
94
|
+
// Catches the same antipattern the slash-command CLIs hit in v0.9.16/0.9.17:
|
|
95
|
+
// `args.includes('--json')` returns false for `--json=yes`, so the flag was
|
|
96
|
+
// silently dropped; `args.indexOf('--file')` returns -1 for `--file=PATH`,
|
|
97
|
+
// so the value was silently ignored; an unknown `--jzon` typo was silently
|
|
98
|
+
// stripped from positional and never surfaced. Each path now exits 2.
|
|
99
|
+
export function validateAndExpandFlags(args, knownBools, knownValues, sub) {
|
|
100
|
+
const out = [];
|
|
101
|
+
const bools = new Set(knownBools);
|
|
102
|
+
const values = new Set(knownValues);
|
|
103
|
+
for (const a of args) {
|
|
104
|
+
if (!a.startsWith('--')) { out.push(a); continue; }
|
|
105
|
+
if (a.includes('=')) {
|
|
106
|
+
const eq = a.indexOf('=');
|
|
107
|
+
const k = a.slice(0, eq);
|
|
108
|
+
const v = a.slice(eq + 1);
|
|
109
|
+
if (bools.has(k)) {
|
|
110
|
+
process.stderr.write(`${sub}: '${k}' is a boolean flag and does not take a value (got '${a}'). Drop the '=...' suffix.\n`);
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
if (values.has(k)) {
|
|
114
|
+
out.push(k);
|
|
115
|
+
out.push(v);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
process.stderr.write(`${sub}: unknown flag '${k}' (got '${a}').\n`);
|
|
119
|
+
process.exit(2);
|
|
120
|
+
}
|
|
121
|
+
if (bools.has(a) || values.has(a)) { out.push(a); continue; }
|
|
122
|
+
process.stderr.write(`${sub}: unknown flag '${a}'.\n`);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
package/scripts/lib/lint.js
CHANGED
|
@@ -85,10 +85,14 @@ function posixClassesToJs(regex) {
|
|
|
85
85
|
// not read as a value claim. `\b` treats '-', '/', '.' as word boundaries, so
|
|
86
86
|
// `\bcomprehensive\b` fires INSIDE `comprehensive-parser.js` or a branch name
|
|
87
87
|
// `docs/comprehensive-audit`. Mirrors hooks/banned-vocab-check.sh's v0.23.19
|
|
88
|
-
// Path 2 sanitizer
|
|
89
|
-
//
|
|
90
|
-
// commit messages
|
|
91
|
-
//
|
|
88
|
+
// Path 2 sanitizer stage for stage: fenced blocks → inline backtick spans →
|
|
89
|
+
// slashed-path runs → bare dotted-file tokens. The last clause was JS-only when
|
|
90
|
+
// it was written — commit messages, the CLI's primary input, commonly name bare
|
|
91
|
+
// files (`refactor comprehensive-parser.js`) with no backticks and no leading
|
|
92
|
+
// path — but the bash side gained it on 2026-08-16 and
|
|
93
|
+
// tests/scripts/sanitize-stage-parity.test.js now extracts both programs and
|
|
94
|
+
// requires the clause lists to match, so it is no longer an extension of
|
|
95
|
+
// anything (audit-2026-08-22 条目 22). Token classes are ASCII-only so 中文
|
|
92
96
|
// prose and bare-word claims (the real violations) stay intact and still match.
|
|
93
97
|
export function stripIdentifiers(text) {
|
|
94
98
|
if (!text) return text;
|
|
@@ -162,8 +166,10 @@ export function stripIdentifiers(text) {
|
|
|
162
166
|
// The bash engines need no equivalent: POSIX sed does not backtrack and
|
|
163
167
|
// the hook caps its input at `tail -c 4096`; the Node path caps nothing.
|
|
164
168
|
.replace(/(?<![A-Za-z0-9._@~-])[A-Za-z0-9._@~-]*\/[A-Za-z0-9._/@~-]*/g, ' ')
|
|
165
|
-
// 4. Bare dotted-file tokens (foo.js, comprehensive-parser.ts)
|
|
166
|
-
//
|
|
169
|
+
// 4. Bare dotted-file tokens (foo.js, comprehensive-parser.ts). JS-only
|
|
170
|
+
// when written; the bash sanitizer carries the same clause since
|
|
171
|
+
// 2026-08-16 and sanitize-stage-parity pins them together.
|
|
172
|
+
// The extension must start with a LOWERCASE letter, which
|
|
167
173
|
// (a) excludes decimals / versions ("3.5x", "v6.14") whose ".5x"/".14"
|
|
168
174
|
// could otherwise swallow a baseline-less ratio claim → false negative,
|
|
169
175
|
// and (b) excludes sentence-boundary typos ("comprehensive.Next", capital
|