claudecode-linter 2.1.150 → 2.1.153

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.
@@ -1,13 +1,13 @@
1
1
  {
2
- "extractedFromClaudeCodeVersion": "2.1.146",
3
- "extractedAt": "2026-05-22T08:16:43.683Z",
2
+ "extractedFromClaudeCodeVersion": "2.1.153",
3
+ "extractedAt": "2026-05-28T01:01:28.673Z",
4
4
  "schema": {
5
5
  "$schema": "https://json-schema.org/draft/2020-12/schema",
6
6
  "title": "Claude Code settings.json",
7
7
  "type": "object",
8
8
  "properties": {
9
9
  "$schema": {
10
- "const": "pXq",
10
+ "const": "https://json.schemastore.org/claude-code-settings.json",
11
11
  "description": "JSON Schema reference for Claude Code settings"
12
12
  },
13
13
  "apiKeyHelper": {
@@ -78,6 +78,48 @@
78
78
  "type": "boolean",
79
79
  "description": "Whether file picker should respect .gitignore files (default: true). Note: .ignore files are always respected."
80
80
  },
81
+ "breakReminder": {
82
+ "type": "object",
83
+ "properties": {
84
+ "enabled": {
85
+ "type": "boolean",
86
+ "description": "Show a friendly nudge after sustained continuous use (default false). Must be true for the reminder to fire."
87
+ },
88
+ "intervalMinutes": {
89
+ "type": "number",
90
+ "description": "Minutes of continuous use before the reminder fires (default 120). Re-fires every interval until you take a break."
91
+ },
92
+ "breakThresholdMinutes": {
93
+ "type": "number",
94
+ "description": "Minutes of inactivity that count as a break and reset the timer (default 15)"
95
+ },
96
+ "message": {
97
+ "type": "string",
98
+ "description": "Custom reminder text. Leave unset for a rotating set of friendly nudges."
99
+ }
100
+ },
101
+ "description": "@internal Opt-in break reminder. When enabled, shows a dismissible nudge after sustained continuous use. Never blocks — just a friendly heads-up."
102
+ },
103
+ "quietHours": {
104
+ "type": "object",
105
+ "properties": {
106
+ "enabled": {
107
+ "type": "boolean",
108
+ "description": "Show a one-time nudge when you start or keep using the CLI inside your quiet-hours window (default false)."
109
+ },
110
+ "start": {
111
+ "type": "string",
112
+ "pattern": "^([01]?\\d|2[0-3]):[0-5]\\d$",
113
+ "description": "Start of the quiet-hours window, 24-hour local time \"HH:MM\"."
114
+ },
115
+ "end": {
116
+ "type": "string",
117
+ "pattern": "^([01]?\\d|2[0-3]):[0-5]\\d$",
118
+ "description": "End of the quiet-hours window, 24-hour local time \"HH:MM\". May be earlier than start for an overnight range."
119
+ }
120
+ },
121
+ "description": "@internal Opt-in quiet hours. When enabled, shows a single soft nudge per session while inside the configured local-time window. Never blocks."
122
+ },
81
123
  "cleanupPeriodDays": {
82
124
  "type": "number",
83
125
  "description": "Number of days to retain chat transcripts before automatic cleanup (default: 30). Minimum 1. Use a large value for long retention; use --no-session-persistence to disable transcript writes entirely."
@@ -531,6 +573,14 @@
531
573
  "type": "boolean",
532
574
  "description": "Disable Remote Control (claude.ai/code, `claude remote-control`, `--remote-control`/`--rc`, auto-start, and the in-session toggle). Typically set in managed settings."
533
575
  },
576
+ "disableWorkflows": {
577
+ "type": "boolean",
578
+ "description": "@internal Disable the Workflows feature (also via CLAUDE_CODE_DISABLE_WORKFLOWS)."
579
+ },
580
+ "enableWorkflows": {
581
+ "type": "boolean",
582
+ "description": "Enable or disable the Workflows feature for this user. Unset = default by plan once the feature is available."
583
+ },
534
584
  "disableSkillShellExecution": {
535
585
  "type": "boolean",
536
586
  "description": "Disable inline shell execution in skills and custom slash commands from user, project, or plugin sources. Commands are replaced with a placeholder instead of being run."
@@ -568,6 +618,10 @@
568
618
  "type": "boolean",
569
619
  "description": "When true (and set in managed settings), allowedMcpServers is only read from managed settings. deniedMcpServers still merges from all sources, so users can deny servers for themselves. Users can still add their own MCP servers, but only the admin-defined allowlist applies."
570
620
  },
621
+ "allowAllClaudeAiMcps": {
622
+ "type": "boolean",
623
+ "description": "When true (and set in managed settings), claude.ai cloud MCP connectors load alongside managed-mcp.json instead of being suppressed by its exclusive-control lockdown. Default off preserves the lockdown. Read from managed settings only."
624
+ },
571
625
  "strictPluginOnlyCustomization": {
572
626
  "anyOf": [
573
627
  {
@@ -710,6 +764,10 @@
710
764
  "type": "string"
711
765
  },
712
766
  "description": "Directories to include via git sparse-checkout (cone mode). Use for monorepos where the marketplace lives in a subdirectory. Example: [\".claude-plugin\", \"plugins\"]. If omitted, the full repository is cloned."
767
+ },
768
+ "skipLfs": {
769
+ "type": "boolean",
770
+ "description": "Skip Git LFS smudge during clone and update (sets GIT_LFS_SKIP_SMUDGE=1) so LFS pointer files stay as pointers instead of downloading their content. Use for marketplaces hosted in repos with large LFS objects."
713
771
  }
714
772
  },
715
773
  "required": [
@@ -741,6 +799,10 @@
741
799
  "type": "string"
742
800
  },
743
801
  "description": "Directories to include via git sparse-checkout (cone mode). Use for monorepos where the marketplace lives in a subdirectory. Example: [\".claude-plugin\", \"plugins\"]. If omitted, the full repository is cloned."
802
+ },
803
+ "skipLfs": {
804
+ "type": "boolean",
805
+ "description": "Skip Git LFS smudge during clone and update (sets GIT_LFS_SKIP_SMUDGE=1) so LFS pointer files stay as pointers instead of downloading their content. Use for marketplaces hosted in repos with large LFS objects."
744
806
  }
745
807
  },
746
808
  "required": [
@@ -1112,6 +1174,10 @@
1112
1174
  "type": "string"
1113
1175
  },
1114
1176
  "description": "Directories to include via git sparse-checkout (cone mode). Use for monorepos where the marketplace lives in a subdirectory. Example: [\".claude-plugin\", \"plugins\"]. If omitted, the full repository is cloned."
1177
+ },
1178
+ "skipLfs": {
1179
+ "type": "boolean",
1180
+ "description": "Skip Git LFS smudge during clone and update (sets GIT_LFS_SKIP_SMUDGE=1) so LFS pointer files stay as pointers instead of downloading their content. Use for marketplaces hosted in repos with large LFS objects."
1115
1181
  }
1116
1182
  },
1117
1183
  "required": [
@@ -1143,6 +1209,10 @@
1143
1209
  "type": "string"
1144
1210
  },
1145
1211
  "description": "Directories to include via git sparse-checkout (cone mode). Use for monorepos where the marketplace lives in a subdirectory. Example: [\".claude-plugin\", \"plugins\"]. If omitted, the full repository is cloned."
1212
+ },
1213
+ "skipLfs": {
1214
+ "type": "boolean",
1215
+ "description": "Skip Git LFS smudge during clone and update (sets GIT_LFS_SKIP_SMUDGE=1) so LFS pointer files stay as pointers instead of downloading their content. Use for marketplaces hosted in repos with large LFS objects."
1146
1216
  }
1147
1217
  },
1148
1218
  "required": [
@@ -1500,6 +1570,10 @@
1500
1570
  "type": "string"
1501
1571
  },
1502
1572
  "description": "Directories to include via git sparse-checkout (cone mode). Use for monorepos where the marketplace lives in a subdirectory. Example: [\".claude-plugin\", \"plugins\"]. If omitted, the full repository is cloned."
1573
+ },
1574
+ "skipLfs": {
1575
+ "type": "boolean",
1576
+ "description": "Skip Git LFS smudge during clone and update (sets GIT_LFS_SKIP_SMUDGE=1) so LFS pointer files stay as pointers instead of downloading their content. Use for marketplaces hosted in repos with large LFS objects."
1503
1577
  }
1504
1578
  },
1505
1579
  "required": [
@@ -1531,6 +1605,10 @@
1531
1605
  "type": "string"
1532
1606
  },
1533
1607
  "description": "Directories to include via git sparse-checkout (cone mode). Use for monorepos where the marketplace lives in a subdirectory. Example: [\".claude-plugin\", \"plugins\"]. If omitted, the full repository is cloned."
1608
+ },
1609
+ "skipLfs": {
1610
+ "type": "boolean",
1611
+ "description": "Skip Git LFS smudge during clone and update (sets GIT_LFS_SKIP_SMUDGE=1) so LFS pointer files stay as pointers instead of downloading their content. Use for marketplaces hosted in repos with large LFS objects."
1534
1612
  }
1535
1613
  },
1536
1614
  "required": [
@@ -1836,6 +1914,13 @@
1836
1914
  },
1837
1915
  "description": "Enterprise blocklist of marketplace sources. When set in managed settings, these exact sources are blocked from being added as marketplaces. The check happens BEFORE downloading, so blocked sources never touch the filesystem."
1838
1916
  },
1917
+ "pluginSuggestionMarketplaces": {
1918
+ "type": "array",
1919
+ "items": {
1920
+ "type": "string"
1921
+ },
1922
+ "description": "Marketplace names whose plugins may surface as contextual install suggestions (relevance-based tips), in addition to the official marketplace. Only honored when set in managed settings (policy scope); the key is ignored in user, project, and local settings. A name only takes effect when the marketplace is registered on the machine AND its registered source is also declared in managed settings, either as the extraKnownMarketplaces entry for that name or as an entry of strictKnownMarketplaces. A marketplace registered from a different source under an allowlisted name is ignored."
1923
+ },
1839
1924
  "forceLoginMethod": {
1840
1925
  "enum": [
1841
1926
  "claudeai",
@@ -2328,12 +2413,16 @@
2328
2413
  },
2329
2414
  "showThinkingSummaries": {
2330
2415
  "type": "boolean",
2331
- "description": "Show thinking summaries in the transcript view (ctrl+o). Default: false."
2416
+ "description": "Request API-side thinking summaries and show them in the conversation and in the transcript view (ctrl+o). Set explicitly to override the default for your install."
2332
2417
  },
2333
2418
  "skipDangerousModePermissionPrompt": {
2334
2419
  "type": "boolean",
2335
2420
  "description": "Whether the user has accepted the bypass permissions mode dialog"
2336
2421
  },
2422
+ "skipWorkflowUsageWarning": {
2423
+ "type": "boolean",
2424
+ "description": "@internal Whether the user has accepted the multi-agent workflow usage warning. Until set, auto permission mode prompts before running a workflow."
2425
+ },
2337
2426
  "disableAutoMode": {
2338
2427
  "enum": [
2339
2428
  "disable"
@@ -1,6 +1,6 @@
1
1
  {
2
- "extractedFromClaudeCodeVersion": "2.1.146",
3
- "extractedAt": "2026-05-22T08:49:59.762Z",
2
+ "extractedFromClaudeCodeVersion": "2.1.153",
3
+ "extractedAt": "2026-05-28T01:01:28.675Z",
4
4
  "schema": {
5
5
  "$schema": "https://json-schema.org/draft/2020-12/schema",
6
6
  "title": "Claude Code SKILL.md frontmatter",
@@ -84,6 +84,60 @@
84
84
  ],
85
85
  "description": "Tools available to the model while this file is active. Comma-separated string or YAML list."
86
86
  },
87
+ "disallowed-tools": {
88
+ "anyOf": [
89
+ {
90
+ "anyOf": [
91
+ {
92
+ "type": "string"
93
+ },
94
+ {
95
+ "type": "number"
96
+ },
97
+ {
98
+ "type": "boolean"
99
+ },
100
+ {
101
+ "type": "null"
102
+ }
103
+ ]
104
+ },
105
+ {
106
+ "type": "array",
107
+ "items": {
108
+ "type": "string"
109
+ }
110
+ }
111
+ ],
112
+ "description": "Tools removed from the model while this file is active. Comma-separated string or YAML list. Cleared when the user sends the next message."
113
+ },
114
+ "disallowedTools": {
115
+ "anyOf": [
116
+ {
117
+ "anyOf": [
118
+ {
119
+ "type": "string"
120
+ },
121
+ {
122
+ "type": "number"
123
+ },
124
+ {
125
+ "type": "boolean"
126
+ },
127
+ {
128
+ "type": "null"
129
+ }
130
+ ]
131
+ },
132
+ {
133
+ "type": "array",
134
+ "items": {
135
+ "type": "string"
136
+ }
137
+ }
138
+ ],
139
+ "description": "Canonical (normalized) alias of `disallowed-tools`."
140
+ },
87
141
  "argument-hint": {
88
142
  "anyOf": [
89
143
  {
package/dist/contracts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Auto-generated from contracts/claude-code-contracts.json
2
- // Claude Code v2.1.150 — extracted 2026-05-23T07:05:23.158Z
2
+ // Claude Code v2.1.153 — extracted 2026-05-28T01:01:24.865Z
3
3
  // Do not edit manually. Run: npm run generate-contracts
4
4
  export const TOOLS = new Set([
5
5
  "Agent",
@@ -56,6 +56,7 @@ export const HOOK_EVENTS = new Set([
56
56
  "ElicitationResult",
57
57
  "FileChanged",
58
58
  "InstructionsLoaded",
59
+ "MessageDisplay",
59
60
  "Notification",
60
61
  "PermissionDenied",
61
62
  "PermissionRequest",
@@ -249,11 +250,13 @@ export const SETTINGS_USER_FIELDS = new Set([
249
250
  "disableBackgroundAgents",
250
251
  "disableRemoteControl",
251
252
  "disableSkillShellExecution",
253
+ "disableWorkflows",
252
254
  "disabledMcpjsonServers",
253
255
  "doneMeansMerged",
254
256
  "editorMode",
255
257
  "effortLevel",
256
258
  "enableAllProjectMcpServers",
259
+ "enableWorkflows",
257
260
  "enabledMcpjsonServers",
258
261
  "enabledPlugins",
259
262
  "env",
@@ -283,6 +286,7 @@ export const SETTINGS_USER_FIELDS = new Set([
283
286
  "permissions",
284
287
  "plansDirectory",
285
288
  "pluginConfigs",
289
+ "pluginSuggestionMarketplaces",
286
290
  "pluginTrustMessage",
287
291
  "policyHelper",
288
292
  "prUrlTemplate",
@@ -306,6 +310,7 @@ export const SETTINGS_USER_FIELDS = new Set([
306
310
  "skipAutoPermissionPrompt",
307
311
  "skipDangerousModePermissionPrompt",
308
312
  "skipWebFetchPreflight",
313
+ "skipWorkflowUsageWarning",
309
314
  "spinnerTipsEnabled",
310
315
  "spinnerTipsOverride",
311
316
  "spinnerVerbs",
package/dist/discovery.js CHANGED
@@ -205,6 +205,25 @@ function discoverInDirectory(dir) {
205
205
  for (const f of monitors) {
206
206
  artifacts.push({ filePath: f, artifactType: "monitors-json" });
207
207
  }
208
+ // .claude-plugin/marketplace.json — schemastore-only artifact.
209
+ const marketplace = join(dir, ".claude-plugin", "marketplace.json");
210
+ if (existsSync(marketplace)) {
211
+ artifacts.push({ filePath: marketplace, artifactType: "marketplace-json" });
212
+ }
213
+ // keybindings.json — usually at ~/.claude/keybindings.json (user scope),
214
+ // but also picked up at project root if present. schemastore-only artifact.
215
+ for (const candidate of [
216
+ join(dir, "keybindings.json"),
217
+ join(dir, ".claude", "keybindings.json"),
218
+ ]) {
219
+ if (existsSync(candidate)) {
220
+ artifacts.push({
221
+ filePath: candidate,
222
+ artifactType: "keybindings-json",
223
+ scope: detectScope(candidate),
224
+ });
225
+ }
226
+ }
208
227
  // Claude config files — settings
209
228
  for (const name of ["settings.json", "settings.local.json"]) {
210
229
  // Direct in dir (handles both ~/.claude/settings.json and project root)
@@ -316,6 +335,10 @@ function classifyFile(filePath) {
316
335
  const parent = basename(dirname(filePath));
317
336
  if (name === "plugin.json" && parent === ".claude-plugin")
318
337
  return "plugin-json";
338
+ if (name === "marketplace.json" && parent === ".claude-plugin")
339
+ return "marketplace-json";
340
+ if (name === "keybindings.json")
341
+ return "keybindings-json";
319
342
  if (name === "SKILL.md")
320
343
  return "skill-md";
321
344
  if (name === "hooks.json" && parent === "hooks")
@@ -33,6 +33,12 @@ export const mcpJsonFixer = {
33
33
  orderedServer[field] = serverObj[field];
34
34
  }
35
35
  }
36
+ // gitea#7: URL-based servers with type:"http" rewrite to
37
+ // "streamable-http" (the runtime transport is identical post-v2.1.146;
38
+ // the rename avoids the legacy OAuth-probe code path).
39
+ if (orderedServer.type === "http" && typeof orderedServer.url === "string") {
40
+ orderedServer.type = "streamable-http";
41
+ }
36
42
  sortedServers[serverName] = orderedServer;
37
43
  }
38
44
  else {
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ import { mcpJsonLinter } from "./linters/mcp-json.js";
21
21
  import { claudeMdLinter } from "./linters/claude-md.js";
22
22
  import { lspJsonLinter } from "./linters/lsp-json.js";
23
23
  import { monitorsJsonLinter } from "./linters/monitors-json.js";
24
+ import { marketplaceJsonLinter } from "./linters/marketplace-json.js";
25
+ import { keybindingsJsonLinter } from "./linters/keybindings-json.js";
24
26
  import { misplacedFileLinter, MISPLACED_FILE_RULES, } from "./linters/misplaced-file.js";
25
27
  import { pluginJsonFixer } from "./fixers/plugin-json.js";
26
28
  import { frontmatterFixer } from "./fixers/frontmatter.js";
@@ -38,6 +40,8 @@ import { MCP_JSON_RULES } from "./linters/mcp-json.js";
38
40
  import { CLAUDE_MD_RULES } from "./linters/claude-md.js";
39
41
  import { LSP_JSON_RULES } from "./linters/lsp-json.js";
40
42
  import { MONITORS_JSON_RULES } from "./linters/monitors-json.js";
43
+ import { MARKETPLACE_JSON_RULES } from "./linters/marketplace-json.js";
44
+ import { KEYBINDINGS_JSON_RULES } from "./linters/keybindings-json.js";
41
45
  const LINTERS = {
42
46
  "plugin-json": pluginJsonLinter,
43
47
  "skill-md": skillMdLinter,
@@ -49,6 +53,8 @@ const LINTERS = {
49
53
  "claude-md": claudeMdLinter,
50
54
  "lsp-json": lspJsonLinter,
51
55
  "monitors-json": monitorsJsonLinter,
56
+ "marketplace-json": marketplaceJsonLinter,
57
+ "keybindings-json": keybindingsJsonLinter,
52
58
  "misplaced-file": misplacedFileLinter,
53
59
  };
54
60
  const FIXERS = {
@@ -72,6 +78,8 @@ const ALL_RULES = [
72
78
  ...CLAUDE_MD_RULES,
73
79
  ...LSP_JSON_RULES,
74
80
  ...MONITORS_JSON_RULES,
81
+ ...MARKETPLACE_JSON_RULES,
82
+ ...KEYBINDINGS_JSON_RULES,
75
83
  ...MISPLACED_FILE_RULES,
76
84
  ];
77
85
  /**
@@ -69,7 +69,9 @@ const RULES = [
69
69
  { id: "agent-md/permission-mode-valid", defaultSeverity: "warning" },
70
70
  { id: "agent-md/effort-valid", defaultSeverity: "warning" },
71
71
  { id: "agent-md/max-turns-valid", defaultSeverity: "warning" },
72
- { id: "agent-md/color-required", defaultSeverity: "warning" },
72
+ // agent-md/color-required removed in gitea#3 — Claude Code marks `color`
73
+ // as `.optional()` and `@internal`. The remaining `color-valid` rule still
74
+ // enforces the value set when an author opts in.
73
75
  { id: "agent-md/color-valid", defaultSeverity: "warning" },
74
76
  { id: "agent-md/system-prompt-present", defaultSeverity: "error" },
75
77
  { id: "agent-md/system-prompt-length", defaultSeverity: "warning" },
@@ -186,12 +188,19 @@ export const agentMdLinter = {
186
188
  push(diag(config, filePath, "agent-md/max-turns-valid", "warning", `"maxTurns" must be a positive integer (got ${JSON.stringify(mt)})`));
187
189
  }
188
190
  }
189
- // color
190
- if (!("color" in fm.data) || typeof fm.data.color !== "string") {
191
- push(diag(config, filePath, "agent-md/color-required", "warning", '"color" is required in frontmatter'));
192
- }
193
- else if (!AGENT_COLORS.has(fm.data.color)) {
194
- push(diag(config, filePath, "agent-md/color-valid", "warning", `"color" must be one of: ${[...AGENT_COLORS].join(", ")} (got "${fm.data.color}")`));
191
+ // color — optional per Claude Code's agent frontmatter Zod schema
192
+ // (`color: hW().optional().describe("@internal display color in the
193
+ // agents UI")`). Only validate the value if the user set one; never
194
+ // require it. The `color-required` rule (gitea#3) is kept on disk for
195
+ // users who explicitly opted in via .claudecode-lint.yaml, but defaults
196
+ // to off because requiring an @internal display field misled users.
197
+ if ("color" in fm.data) {
198
+ if (typeof fm.data.color !== "string") {
199
+ push(diag(config, filePath, "agent-md/color-valid", "warning", '"color" must be a string when set'));
200
+ }
201
+ else if (!AGENT_COLORS.has(fm.data.color)) {
202
+ push(diag(config, filePath, "agent-md/color-valid", "warning", `"color" must be one of: ${[...AGENT_COLORS].join(", ")} (got "${fm.data.color}")`));
203
+ }
195
204
  }
196
205
  // Frontmatter keys: only flag cross-artifact misplacement. A key valid
197
206
  // for a *different* markdown artifact (e.g. a skill-only key on an
@@ -0,0 +1,53 @@
1
+ import { formatAjvError, loadKeybindingsSchema, summarizeErrors, } from "../plugin-schema.js";
2
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
3
+ export const KEYBINDINGS_JSON_RULES = [
4
+ { id: "keybindings-json/valid-json", defaultSeverity: "error" },
5
+ { id: "keybindings-json/schema-valid", defaultSeverity: "error" },
6
+ ];
7
+ /**
8
+ * Validate `~/.claude/keybindings.json` (or per-project `keybindings.json`)
9
+ * against the schemastore.org curated schema. As with marketplace.json,
10
+ * there is no Zod source for keybindings in the Claude Code bundle —
11
+ * schemastore is the sole authoritative shape we have.
12
+ */
13
+ export const keybindingsJsonLinter = {
14
+ artifactType: "keybindings-json",
15
+ lint(filePath, content, config) {
16
+ const diagnostics = [];
17
+ const push = (d) => {
18
+ if (d)
19
+ diagnostics.push(d);
20
+ };
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(content);
24
+ }
25
+ catch (e) {
26
+ push(diag(config, filePath, "keybindings-json/valid-json", "error", `Invalid JSON: ${e.message}`));
27
+ return diagnostics;
28
+ }
29
+ if (isRuleEnabled(config, "keybindings-json/schema-valid")) {
30
+ const compiled = loadKeybindingsSchema();
31
+ if (compiled) {
32
+ const ok = compiled.validate(parsed);
33
+ if (!ok && compiled.validate.errors) {
34
+ for (const err of summarizeErrors(compiled.validate.errors)) {
35
+ push(diag(config, filePath, "keybindings-json/schema-valid", "error", formatAjvError(err)));
36
+ }
37
+ }
38
+ }
39
+ }
40
+ return diagnostics;
41
+ },
42
+ };
43
+ function diag(config, filePath, ruleId, defaultSeverity, message) {
44
+ if (!isRuleEnabled(config, ruleId))
45
+ return null;
46
+ return {
47
+ rule: ruleId,
48
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
49
+ message,
50
+ file: filePath,
51
+ };
52
+ }
53
+ //# sourceMappingURL=keybindings-json.js.map
@@ -0,0 +1,55 @@
1
+ import { formatAjvError, loadMarketplaceSchema, summarizeErrors, } from "../plugin-schema.js";
2
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
3
+ export const MARKETPLACE_JSON_RULES = [
4
+ { id: "marketplace-json/valid-json", defaultSeverity: "error" },
5
+ { id: "marketplace-json/schema-valid", defaultSeverity: "error" },
6
+ ];
7
+ /**
8
+ * Validate `.claude-plugin/marketplace.json` against the schemastore.org
9
+ * curated schema. There's no Zod source for marketplace.json in the Claude
10
+ * Code bundle — schemastore is the sole authoritative shape we have.
11
+ *
12
+ * If the schemastore bundle isn't shipped with this install (unlikely;
13
+ * package.json includes it), the schema check silently skips.
14
+ */
15
+ export const marketplaceJsonLinter = {
16
+ artifactType: "marketplace-json",
17
+ lint(filePath, content, config) {
18
+ const diagnostics = [];
19
+ const push = (d) => {
20
+ if (d)
21
+ diagnostics.push(d);
22
+ };
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(content);
26
+ }
27
+ catch (e) {
28
+ push(diag(config, filePath, "marketplace-json/valid-json", "error", `Invalid JSON: ${e.message}`));
29
+ return diagnostics;
30
+ }
31
+ if (isRuleEnabled(config, "marketplace-json/schema-valid")) {
32
+ const compiled = loadMarketplaceSchema();
33
+ if (compiled) {
34
+ const ok = compiled.validate(parsed);
35
+ if (!ok && compiled.validate.errors) {
36
+ for (const err of summarizeErrors(compiled.validate.errors)) {
37
+ push(diag(config, filePath, "marketplace-json/schema-valid", "error", formatAjvError(err)));
38
+ }
39
+ }
40
+ }
41
+ }
42
+ return diagnostics;
43
+ },
44
+ };
45
+ function diag(config, filePath, ruleId, defaultSeverity, message) {
46
+ if (!isRuleEnabled(config, ruleId))
47
+ return null;
48
+ return {
49
+ rule: ruleId,
50
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
51
+ message,
52
+ file: filePath,
53
+ };
54
+ }
55
+ //# sourceMappingURL=marketplace-json.js.map
@@ -19,6 +19,7 @@ const RULES = [
19
19
  { id: "mcp-json/url-protocol", defaultSeverity: "warning" },
20
20
  { id: "mcp-json/url-valid", defaultSeverity: "error" },
21
21
  { id: "mcp-json/type-matches-transport", defaultSeverity: "warning" },
22
+ { id: "mcp-json/prefer-streamable-http", defaultSeverity: "warning" },
22
23
  { id: "mcp-json/command-args-split", defaultSeverity: "info" },
23
24
  { id: "mcp-json/args-array", defaultSeverity: "error" },
24
25
  { id: "mcp-json/env-object", defaultSeverity: "error" },
@@ -143,6 +144,13 @@ export const mcpJsonLinter = {
143
144
  if ("type" in server && !HTTP_TRANSPORT_TYPES.has(server.type)) {
144
145
  push(diag(config, filePath, "mcp-json/type-matches-transport", "warning", `Server "${name}" has URL but type is "${server.type}" (expected one of ${[...HTTP_TRANSPORT_TYPES].join(", ")})`, sp?.line, sp?.column));
145
146
  }
147
+ // Prefer "streamable-http" over "http" for URL-based MCP servers.
148
+ // Per gitea#7: older Claude Code's "http" transport may trigger an
149
+ // OAuth metadata probe / DCR POST that "streamable-http" skips, and
150
+ // misbehaving upstream proxies can silently fail with "0 tools".
151
+ if (server.type === "http") {
152
+ push(diag(config, filePath, "mcp-json/prefer-streamable-http", "warning", `Server "${name}" uses type "http" — prefer "streamable-http" to avoid OAuth probe edge cases on some upstream proxies`, sp?.line, sp?.column));
153
+ }
146
154
  }
147
155
  // stdio server checks
148
156
  if (hasCommand && !hasUrl) {
@@ -22,6 +22,7 @@ const RULES = [
22
22
  { id: "plugin-json/keywords-no-duplicates", defaultSeverity: "warning" },
23
23
  { id: "plugin-json/no-unknown-fields", defaultSeverity: "info" },
24
24
  { id: "plugin-json/license-spdx", defaultSeverity: "info" },
25
+ { id: "plugin-json/no-inline-mcp-servers", defaultSeverity: "warning" },
25
26
  ];
26
27
  function findKeyPosition(content, key) {
27
28
  const re = new RegExp(`"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*:`);
@@ -178,6 +179,13 @@ export const pluginJsonLinter = {
178
179
  }
179
180
  }
180
181
  }
182
+ // gitea#9: mcpServers belongs in `.mcp.json` at the plugin root, not as
183
+ // a top-level key in `.claude-plugin/plugin.json`. The legacy shape still
184
+ // loads but uses a different code path and contradicts the official docs.
185
+ if ("mcpServers" in parsed) {
186
+ const p = pos("mcpServers");
187
+ push(diag(config, filePath, "plugin-json/no-inline-mcp-servers", "warning", "\"mcpServers\" should not live inside plugin.json — move to .mcp.json at the plugin root. See https://code.claude.com/docs/en/plugins#plugin-structure-overview", p?.line, p?.column));
188
+ }
181
189
  // license
182
190
  if ("license" in parsed && typeof parsed.license === "string") {
183
191
  if (!SPDX_COMMON.has(parsed.license)) {