claudecode-linter 2.1.138-patch.1 → 2.1.138-patch.2

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/dist/discovery.js CHANGED
@@ -173,6 +173,19 @@ function discoverInDirectory(dir) {
173
173
  for (const f of hooks) {
174
174
  artifacts.push({ filePath: f, artifactType: "hooks-json" });
175
175
  }
176
+ // .lsp.json (flat record at plugin root; Claude Code reads from <plugin>/.lsp.json)
177
+ const lspDot = join(dir, ".lsp.json");
178
+ if (existsSync(lspDot)) {
179
+ artifacts.push({ filePath: lspDot, artifactType: "lsp-json" });
180
+ }
181
+ // monitors/monitors.json
182
+ const monitors = globSync("monitors/monitors.json", {
183
+ cwd: dir,
184
+ absolute: true,
185
+ });
186
+ for (const f of monitors) {
187
+ artifacts.push({ filePath: f, artifactType: "monitors-json" });
188
+ }
176
189
  // Claude config files — settings
177
190
  for (const name of ["settings.json", "settings.local.json"]) {
178
191
  // Direct in dir (handles both ~/.claude/settings.json and project root)
@@ -283,6 +296,10 @@ function classifyFile(filePath) {
283
296
  return "skill-md";
284
297
  if (name === "hooks.json" && parent === "hooks")
285
298
  return "hooks-json";
299
+ if (name === ".lsp.json")
300
+ return "lsp-json";
301
+ if (name === "monitors.json" && parent === "monitors")
302
+ return "monitors-json";
286
303
  if (name.endsWith(".md") && parent === "agents")
287
304
  return "agent-md";
288
305
  if (name.endsWith(".md") && parent === "commands")
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ import { hooksJsonLinter } from "./linters/hooks-json.js";
16
16
  import { settingsJsonLinter } from "./linters/settings-json.js";
17
17
  import { mcpJsonLinter } from "./linters/mcp-json.js";
18
18
  import { claudeMdLinter } from "./linters/claude-md.js";
19
+ import { lspJsonLinter } from "./linters/lsp-json.js";
20
+ import { monitorsJsonLinter } from "./linters/monitors-json.js";
19
21
  import { misplacedFileLinter, MISPLACED_FILE_RULES, } from "./linters/misplaced-file.js";
20
22
  import { pluginJsonFixer } from "./fixers/plugin-json.js";
21
23
  import { frontmatterFixer } from "./fixers/frontmatter.js";
@@ -31,6 +33,8 @@ import { HOOKS_JSON_RULES } from "./linters/hooks-json.js";
31
33
  import { SETTINGS_JSON_RULES } from "./linters/settings-json.js";
32
34
  import { MCP_JSON_RULES } from "./linters/mcp-json.js";
33
35
  import { CLAUDE_MD_RULES } from "./linters/claude-md.js";
36
+ import { LSP_JSON_RULES } from "./linters/lsp-json.js";
37
+ import { MONITORS_JSON_RULES } from "./linters/monitors-json.js";
34
38
  const LINTERS = {
35
39
  "plugin-json": pluginJsonLinter,
36
40
  "skill-md": skillMdLinter,
@@ -40,6 +44,8 @@ const LINTERS = {
40
44
  "settings-json": settingsJsonLinter,
41
45
  "mcp-json": mcpJsonLinter,
42
46
  "claude-md": claudeMdLinter,
47
+ "lsp-json": lspJsonLinter,
48
+ "monitors-json": monitorsJsonLinter,
43
49
  "misplaced-file": misplacedFileLinter,
44
50
  };
45
51
  const FIXERS = {
@@ -61,6 +67,8 @@ const ALL_RULES = [
61
67
  ...SETTINGS_JSON_RULES,
62
68
  ...MCP_JSON_RULES,
63
69
  ...CLAUDE_MD_RULES,
70
+ ...LSP_JSON_RULES,
71
+ ...MONITORS_JSON_RULES,
64
72
  ...MISPLACED_FILE_RULES,
65
73
  ];
66
74
  function simpleDiff(oldContent, newContent, filePath) {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Lints standalone `.lsp.json` files (a flat map of server-name → LSP server
3
+ * config). Validates against the schema auto-extracted from Claude Code's
4
+ * runtime parser `E.record(E.string(), RSH()).safeParse(content)` — see
5
+ * scripts/extract-plugin-schema.ts → buildLspSchema().
6
+ *
7
+ * The most common mistake (observed in cluster plugin 0.29.0 → 0.30.3) is
8
+ * wrapping the contents under a top-level `lspServers` key. That wrapper only
9
+ * belongs inside plugin.json; a dedicated `.lsp.json` file must be flat.
10
+ * lsp-json/no-lsp-servers-wrapper catches this with a friendlier message
11
+ * than the generic "missing required field command" Ajv would otherwise emit.
12
+ */
13
+ import { formatAjvError, loadLspSchema, summarizeErrors, } from "../plugin-schema.js";
14
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
15
+ const RULES = [
16
+ { id: "lsp-json/valid-json", defaultSeverity: "error" },
17
+ { id: "lsp-json/no-lsp-servers-wrapper", defaultSeverity: "error" },
18
+ { id: "lsp-json/schema-valid", defaultSeverity: "error" },
19
+ ];
20
+ function diag(config, filePath, ruleId, defaultSeverity, message, line, column) {
21
+ if (!isRuleEnabled(config, ruleId))
22
+ return null;
23
+ return {
24
+ rule: ruleId,
25
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
26
+ message,
27
+ file: filePath,
28
+ line,
29
+ column,
30
+ };
31
+ }
32
+ function findKeyPosition(content, key) {
33
+ const re = new RegExp(`"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*:`);
34
+ const match = re.exec(content);
35
+ if (!match)
36
+ return undefined;
37
+ const before = content.slice(0, match.index);
38
+ const line = before.split("\n").length;
39
+ const lastNl = before.lastIndexOf("\n");
40
+ const column = match.index - lastNl;
41
+ return { line, column };
42
+ }
43
+ export const lspJsonLinter = {
44
+ artifactType: "lsp-json",
45
+ lint(filePath, content, config) {
46
+ const diagnostics = [];
47
+ const push = (d) => {
48
+ if (d)
49
+ diagnostics.push(d);
50
+ };
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(content);
54
+ }
55
+ catch (e) {
56
+ push(diag(config, filePath, "lsp-json/valid-json", "error", `Invalid JSON: ${e.message}`));
57
+ return diagnostics;
58
+ }
59
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
60
+ push(diag(config, filePath, "lsp-json/valid-json", "error", ".lsp.json must be a JSON object (map of server-name → config)"));
61
+ return diagnostics;
62
+ }
63
+ const obj = parsed;
64
+ // Special-case the cluster-plugin bug: wrapping content under "lspServers".
65
+ // That key is only valid inline in plugin.json. The dedicated file uses a
66
+ // flat map, so an "lspServers" wrapper is silently mis-validated by Claude
67
+ // Code at session start (each server entry fails RSH validation since it
68
+ // looks like one server config with sub-server keys).
69
+ if ("lspServers" in obj) {
70
+ const p = findKeyPosition(content, "lspServers");
71
+ push(diag(config, filePath, "lsp-json/no-lsp-servers-wrapper", "error", '.lsp.json must not have a top-level "lspServers" key — the file is itself the map of server-name → config. The "lspServers" wrapper only belongs in plugin.json under the lspServers field.', p?.line, p?.column));
72
+ }
73
+ // Schema validation — defers to the extracted Zod-equivalent JSON Schema.
74
+ if (isRuleEnabled(config, "lsp-json/schema-valid")) {
75
+ const ctx = loadLspSchema();
76
+ if (ctx) {
77
+ const ok = ctx.validate(parsed);
78
+ if (!ok && ctx.validate.errors) {
79
+ const filtered = summarizeErrors(ctx.validate.errors);
80
+ for (const err of filtered) {
81
+ const firstSeg = err.instancePath
82
+ .split("/")
83
+ .filter(Boolean)[0];
84
+ const p = firstSeg ? findKeyPosition(content, firstSeg) : undefined;
85
+ push(diag(config, filePath, "lsp-json/schema-valid", "error", formatAjvError(err), p?.line, p?.column));
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return diagnostics;
91
+ },
92
+ };
93
+ export { RULES as LSP_JSON_RULES };
94
+ //# sourceMappingURL=lsp-json.js.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Lints standalone `monitors/monitors.json` files (an array of monitor
3
+ * entries). Validates against the schema auto-extracted from Claude Code's
4
+ * runtime parser `vC8().parse(content)` — see scripts/extract-plugin-schema.ts
5
+ * → buildMonitorsSchema().
6
+ *
7
+ * monitors-json/unique-names mirrors Claude Code's refine() check that
8
+ * `Monitor names must be unique within a plugin`. JSON Schema can't express
9
+ * that constraint natively so we enforce it here.
10
+ */
11
+ import { formatAjvError, loadMonitorsSchema, summarizeErrors, } from "../plugin-schema.js";
12
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
13
+ const RULES = [
14
+ { id: "monitors-json/valid-json", defaultSeverity: "error" },
15
+ { id: "monitors-json/schema-valid", defaultSeverity: "error" },
16
+ { id: "monitors-json/unique-names", defaultSeverity: "error" },
17
+ ];
18
+ function diag(config, filePath, ruleId, defaultSeverity, message, line, column) {
19
+ if (!isRuleEnabled(config, ruleId))
20
+ return null;
21
+ return {
22
+ rule: ruleId,
23
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
24
+ message,
25
+ file: filePath,
26
+ line,
27
+ column,
28
+ };
29
+ }
30
+ export const monitorsJsonLinter = {
31
+ artifactType: "monitors-json",
32
+ lint(filePath, content, config) {
33
+ const diagnostics = [];
34
+ const push = (d) => {
35
+ if (d)
36
+ diagnostics.push(d);
37
+ };
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(content);
41
+ }
42
+ catch (e) {
43
+ push(diag(config, filePath, "monitors-json/valid-json", "error", `Invalid JSON: ${e.message}`));
44
+ return diagnostics;
45
+ }
46
+ if (!Array.isArray(parsed)) {
47
+ push(diag(config, filePath, "monitors-json/valid-json", "error", "monitors.json must be a JSON array of monitor entries"));
48
+ return diagnostics;
49
+ }
50
+ // Schema validation
51
+ if (isRuleEnabled(config, "monitors-json/schema-valid")) {
52
+ const ctx = loadMonitorsSchema();
53
+ if (ctx) {
54
+ const ok = ctx.validate(parsed);
55
+ if (!ok && ctx.validate.errors) {
56
+ const filtered = summarizeErrors(ctx.validate.errors);
57
+ for (const err of filtered) {
58
+ push(diag(config, filePath, "monitors-json/schema-valid", "error", formatAjvError(err)));
59
+ }
60
+ }
61
+ }
62
+ }
63
+ // Unique-name refinement — Claude Code's refine() check.
64
+ if (isRuleEnabled(config, "monitors-json/unique-names")) {
65
+ const seen = new Set();
66
+ const duplicates = new Set();
67
+ for (const entry of parsed) {
68
+ if (entry &&
69
+ typeof entry === "object" &&
70
+ typeof entry.name === "string") {
71
+ const name = entry.name;
72
+ if (seen.has(name))
73
+ duplicates.add(name);
74
+ seen.add(name);
75
+ }
76
+ }
77
+ for (const dup of duplicates) {
78
+ push(diag(config, filePath, "monitors-json/unique-names", "error", `Monitor name "${dup}" is duplicated. Monitor names must be unique within a plugin.`));
79
+ }
80
+ }
81
+ return diagnostics;
82
+ },
83
+ };
84
+ export { RULES as MONITORS_JSON_RULES };
85
+ //# sourceMappingURL=monitors-json.js.map
@@ -1,5 +1,6 @@
1
1
  import semver from "semver";
2
2
  import { PLUGIN_JSON_FIELDS } from "../contracts.js";
3
+ import { formatAjvError, loadPluginSchema, summarizeErrors, } from "../plugin-schema.js";
3
4
  import { isRuleEnabled, getRuleSeverity } from "../types.js";
4
5
  import { isKebabCase } from "../utils/kebab-case.js";
5
6
  const SPDX_COMMON = new Set([
@@ -9,6 +10,7 @@ const SPDX_COMMON = new Set([
9
10
  ]);
10
11
  const RULES = [
11
12
  { id: "plugin-json/valid-json", defaultSeverity: "error" },
13
+ { id: "plugin-json/schema-valid", defaultSeverity: "error" },
12
14
  { id: "plugin-json/name-required", defaultSeverity: "error" },
13
15
  { id: "plugin-json/name-kebab-case", defaultSeverity: "error" },
14
16
  { id: "plugin-json/name-length", defaultSeverity: "error" },
@@ -64,6 +66,27 @@ export const pluginJsonLinter = {
64
66
  push(diag(config, filePath, "plugin-json/valid-json", "error", "plugin.json must be a JSON object"));
65
67
  return diagnostics;
66
68
  }
69
+ // schema-valid — defer to extracted JSON Schema for type/enum/nested checks
70
+ // that the per-field rules below don't cover. Existing rules (name-required,
71
+ // version-semver, …) handle the common cases with friendlier messages, so
72
+ // most users will see schema-valid fire only on deeper structural issues
73
+ // (MCP transport unions, userConfig shape, etc.). Skipped silently if the
74
+ // schema bundle isn't shipped with this install.
75
+ if (isRuleEnabled(config, "plugin-json/schema-valid")) {
76
+ const ctx = loadPluginSchema();
77
+ if (ctx) {
78
+ const ok = ctx.validate(parsed);
79
+ if (!ok && ctx.validate.errors) {
80
+ const filtered = summarizeErrors(ctx.validate.errors);
81
+ for (const err of filtered) {
82
+ // First path segment maps to a top-level field we can highlight.
83
+ const firstSeg = err.instancePath.split("/").filter(Boolean)[0];
84
+ const p = firstSeg ? pos(firstSeg) : undefined;
85
+ push(diag(config, filePath, "plugin-json/schema-valid", "error", formatAjvError(err), p?.line, p?.column));
86
+ }
87
+ }
88
+ }
89
+ }
67
90
  // name
68
91
  if (!("name" in parsed) || typeof parsed.name !== "string" || parsed.name === "") {
69
92
  push(diag(config, filePath, "plugin-json/name-required", "error", "\"name\" field is required and must be a non-empty string"));
@@ -142,11 +165,17 @@ export const pluginJsonLinter = {
142
165
  }
143
166
  }
144
167
  }
145
- // unknown fields
146
- for (const key of Object.keys(parsed)) {
147
- if (!PLUGIN_JSON_FIELDS.has(key)) {
148
- const p = pos(key);
149
- push(diag(config, filePath, "plugin-json/no-unknown-fields", "info", `Unknown field "${key}" (known: ${[...PLUGIN_JSON_FIELDS].join(", ")})`, p?.line, p?.column));
168
+ // unknown fields — prefer the schema's property set (full + nested-aware)
169
+ // when available; fall back to the contract-extracted PLUGIN_JSON_FIELDS
170
+ // for installs that didn't ship the schema bundle.
171
+ {
172
+ const ctx = loadPluginSchema();
173
+ const known = ctx?.knownFields ?? PLUGIN_JSON_FIELDS;
174
+ for (const key of Object.keys(parsed)) {
175
+ if (!known.has(key)) {
176
+ const p = pos(key);
177
+ push(diag(config, filePath, "plugin-json/no-unknown-fields", "info", `Unknown field "${key}" (known: ${[...known].sort().join(", ")})`, p?.line, p?.column));
178
+ }
150
179
  }
151
180
  }
152
181
  // license
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Lazy loader for the auto-extracted plugin.json JSON Schema. The schema lives
3
+ * at `contracts/plugin.schema.json` (extracted from Claude Code's cli.js by
4
+ * `scripts/extract-plugin-schema.ts`).
5
+ *
6
+ * Resolution: from dist/plugin-schema.js (compiled), step up two directories to
7
+ * reach the package root where `contracts/` lives. Falls back to the package
8
+ * resolver if the file is being run from an alternate layout (e.g. monorepo).
9
+ */
10
+ import { readFileSync } from "node:fs";
11
+ import { dirname, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { Ajv2020 } from "ajv/dist/2020.js";
14
+ // biome-ignore lint/style/useImportType: runtime import; types only.
15
+ import * as addFormatsNs from "ajv-formats";
16
+ // ajv-formats ships as CJS with a default function export. Under Node16
17
+ // ESM resolution we have to reach in for `.default`.
18
+ const addFormats = addFormatsNs.default;
19
+ let cached = null;
20
+ const compiledCache = new Map();
21
+ let ajvShared = null;
22
+ function getAjv() {
23
+ if (ajvShared)
24
+ return ajvShared;
25
+ ajvShared = new Ajv2020({ allErrors: true, strict: false });
26
+ addFormats(ajvShared);
27
+ return ajvShared;
28
+ }
29
+ function loadCompiledSchema(fileName) {
30
+ if (compiledCache.has(fileName))
31
+ return compiledCache.get(fileName) ?? null;
32
+ const here = dirname(fileURLToPath(import.meta.url));
33
+ const candidates = [
34
+ resolve(here, "..", "contracts", fileName),
35
+ resolve(here, "..", "..", "contracts", fileName),
36
+ ];
37
+ let raw = null;
38
+ for (const p of candidates) {
39
+ try {
40
+ raw = readFileSync(p, "utf8");
41
+ break;
42
+ }
43
+ catch {
44
+ // try next
45
+ }
46
+ }
47
+ if (!raw) {
48
+ compiledCache.set(fileName, null);
49
+ return null;
50
+ }
51
+ const wrapped = JSON.parse(raw);
52
+ const compiled = {
53
+ validate: getAjv().compile(wrapped.schema),
54
+ extractedFromVersion: wrapped.extractedFromClaudeCodeVersion,
55
+ };
56
+ compiledCache.set(fileName, compiled);
57
+ return compiled;
58
+ }
59
+ export function loadLspSchema() {
60
+ return loadCompiledSchema("lsp.schema.json");
61
+ }
62
+ export function loadMonitorsSchema() {
63
+ return loadCompiledSchema("monitors.schema.json");
64
+ }
65
+ export function loadPluginSchema() {
66
+ if (cached)
67
+ return cached;
68
+ const here = dirname(fileURLToPath(import.meta.url));
69
+ const candidates = [
70
+ resolve(here, "..", "contracts", "plugin.schema.json"),
71
+ resolve(here, "..", "..", "contracts", "plugin.schema.json"),
72
+ ];
73
+ let raw = null;
74
+ for (const p of candidates) {
75
+ try {
76
+ raw = readFileSync(p, "utf8");
77
+ break;
78
+ }
79
+ catch {
80
+ // try next
81
+ }
82
+ }
83
+ if (!raw)
84
+ return null;
85
+ const wrapped = JSON.parse(raw);
86
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
87
+ addFormats(ajv);
88
+ const validate = ajv.compile(wrapped.schema);
89
+ const knownFields = new Set(Object.keys(wrapped.schema.properties ?? {}));
90
+ cached = {
91
+ validate,
92
+ extractedFromVersion: wrapped.extractedFromClaudeCodeVersion,
93
+ knownFields,
94
+ };
95
+ return cached;
96
+ }
97
+ /**
98
+ * Reduce Ajv's anyOf/oneOf branch enumeration to the "closest match" branch.
99
+ *
100
+ * When a value fails an anyOf with N branches, Ajv emits errors for every
101
+ * branch — that's faithful but noisy (14 lines for one malformed MCP server).
102
+ * We use schemaPath to group child errors by branch index and keep only the
103
+ * branch with the fewest errors (the most-likely-intended shape).
104
+ *
105
+ * Nested unions are handled by processing outer unions first: their kept
106
+ * branch's inner anyOf and inner children survive into the second pass, where
107
+ * the inner union is itself summarized.
108
+ */
109
+ export function summarizeErrors(errors) {
110
+ if (errors.length === 0)
111
+ return errors;
112
+ // Sort unions outermost-first (shorter schemaPath = closer to root).
113
+ const unions = errors
114
+ .filter((e) => e.keyword === "anyOf" || e.keyword === "oneOf")
115
+ .sort((a, b) => a.schemaPath.length - b.schemaPath.length);
116
+ const dropped = new Set();
117
+ for (const union of unions) {
118
+ // Children of this union have schemaPath starting with `<unionPath>/<N>/`.
119
+ // AJV emits schemaPath like "#/properties/mcpServers/anyOf" for the union
120
+ // summary and "#/properties/mcpServers/anyOf/2/required" for branch 2's
121
+ // child errors.
122
+ const prefix = `${union.schemaPath}/`;
123
+ const children = errors.filter((e) => e !== union && e.schemaPath.startsWith(prefix) && !dropped.has(e));
124
+ if (children.length === 0)
125
+ continue;
126
+ // Group by branch index (the segment right after the prefix).
127
+ const byBranch = new Map();
128
+ for (const c of children) {
129
+ const branchIdx = c.schemaPath.slice(prefix.length).split("/", 1)[0];
130
+ const arr = byBranch.get(branchIdx) ?? [];
131
+ arr.push(c);
132
+ byBranch.set(branchIdx, arr);
133
+ }
134
+ // Pick the branch the user got the furthest into. We measure "furthest"
135
+ // as the depth of each branch's *shallowest* error: a branch that failed
136
+ // only deep inside means we matched the outer shape (e.g. value is an
137
+ // object, but a nested transport type is wrong) — strong signal of intent.
138
+ // Counting errors flatly would mislead since nested anyOfs fan out.
139
+ let bestBranch = null;
140
+ let bestDepth = -1;
141
+ for (const [branch, errs] of byBranch) {
142
+ let minDepth = Infinity;
143
+ for (const e of errs) {
144
+ const depth = e.schemaPath
145
+ .slice(prefix.length + branch.length + 1)
146
+ .split("/").length;
147
+ if (depth < minDepth)
148
+ minDepth = depth;
149
+ }
150
+ if (minDepth > bestDepth) {
151
+ bestDepth = minDepth;
152
+ bestBranch = branch;
153
+ }
154
+ }
155
+ // Drop every child of this union that isn't in the best branch.
156
+ for (const [branch, errs] of byBranch) {
157
+ if (branch === bestBranch)
158
+ continue;
159
+ for (const e of errs)
160
+ dropped.add(e);
161
+ }
162
+ // The anyOf/oneOf parent error itself ("does not match any allowed shape")
163
+ // is redundant once we've kept a branch-specific child error that points
164
+ // to the actual problem. Drop it unless its best branch had nothing left.
165
+ const survivingKept = bestBranch !== null
166
+ ? (byBranch.get(bestBranch) ?? []).filter((e) => !dropped.has(e))
167
+ : [];
168
+ if (survivingKept.length > 0)
169
+ dropped.add(union);
170
+ }
171
+ // Final pass: drop the rejected branches and dedupe identical errors.
172
+ const seen = new Set();
173
+ const out = [];
174
+ for (const e of errors) {
175
+ if (dropped.has(e))
176
+ continue;
177
+ const sig = `${e.instancePath}|${e.keyword}|${JSON.stringify(e.params)}`;
178
+ if (seen.has(sig))
179
+ continue;
180
+ seen.add(sig);
181
+ out.push(e);
182
+ }
183
+ return out;
184
+ }
185
+ /** Format an Ajv error as a human-readable single-line message. */
186
+ export function formatAjvError(error) {
187
+ const where = error.instancePath || "(root)";
188
+ const params = error.params;
189
+ switch (error.keyword) {
190
+ case "required":
191
+ return `${where}: missing required field "${params.missingProperty}"`;
192
+ case "type":
193
+ return `${where}: must be ${params.type}`;
194
+ case "enum": {
195
+ const allowed = params.allowedValues ?? [];
196
+ return `${where}: must be one of [${allowed
197
+ .map((v) => JSON.stringify(v))
198
+ .join(", ")}]`;
199
+ }
200
+ case "const":
201
+ return `${where}: must equal ${JSON.stringify(params.allowedValue)}`;
202
+ case "additionalProperties":
203
+ return `${where}: unknown property "${params.additionalProperty}"`;
204
+ case "anyOf":
205
+ return `${where}: does not match any allowed shape`;
206
+ case "oneOf":
207
+ return `${where}: does not match exactly one allowed shape`;
208
+ case "pattern":
209
+ return `${where}: does not match pattern ${params.pattern}`;
210
+ case "minLength":
211
+ return `${where}: must be at least ${params.limit} characters`;
212
+ case "maxLength":
213
+ return `${where}: must be at most ${params.limit} characters`;
214
+ case "minItems":
215
+ return `${where}: must have at least ${params.limit} items`;
216
+ case "maxItems":
217
+ return `${where}: must have at most ${params.limit} items`;
218
+ default:
219
+ return `${where}: ${error.message ?? error.keyword}`;
220
+ }
221
+ }
222
+ //# sourceMappingURL=plugin-schema.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudecode-linter",
3
- "version": "2.1.138-patch.1",
3
+ "version": "2.1.138-patch.2",
4
4
  "description": "Standalone linter for Claude Code plugins and configuration files",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,11 +17,15 @@
17
17
  "deps:update": "ncu -u",
18
18
  "knip": "knip --exclude types",
19
19
  "check-deps": "tsx scripts/check-deps.ts",
20
- "extract-contracts": "tsx scripts/extract-contracts.ts",
20
+ "extract-contracts": "tsx scripts/extract-contracts.ts && tsx scripts/extract-plugin-schema.ts",
21
+ "extract-plugin-schema": "tsx scripts/extract-plugin-schema.ts",
21
22
  "generate-contracts": "tsx scripts/generate-contracts.ts"
22
23
  },
23
24
  "files": [
24
25
  "dist/**/*.js",
26
+ "contracts/plugin.schema.json",
27
+ "contracts/lsp.schema.json",
28
+ "contracts/monitors.schema.json",
25
29
  ".claudecode-lint.defaults.yaml",
26
30
  "README.md"
27
31
  ],
@@ -45,6 +49,8 @@
45
49
  "node": ">=18"
46
50
  },
47
51
  "dependencies": {
52
+ "ajv": "^8.20.0",
53
+ "ajv-formats": "^3.0.1",
48
54
  "minimatch": "^10.2.4",
49
55
  "picocolors": "^1.1.1",
50
56
  "prettier": "^3.8.1",