@lousy-agents/cli 5.19.0 → 5.19.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.
@@ -18,14 +18,14 @@
18
18
  },
19
19
  "devDependencies": {
20
20
  "@biomejs/biome": "2.5.3",
21
- "@lousy-agents/mcp": "5.18.2",
21
+ "@lousy-agents/mcp": "5.19.0",
22
22
  "@modelcontextprotocol/server-sequential-thinking": "2026.7.4",
23
23
  "@testcontainers/postgresql": "12.0.4",
24
24
  "@types/node": "24.13.3",
25
25
  "@upstash/context7-mcp": "3.2.5",
26
26
  "chance": "1.1.13",
27
27
  "testcontainers": "12.0.4",
28
- "tsx": "4.23.4",
28
+ "tsx": "4.23.5",
29
29
  "typescript": "7.0.2",
30
30
  "vitest": "4.1.10"
31
31
  }
@@ -972,9 +972,9 @@
972
972
  }
973
973
  },
974
974
  "node_modules/@lousy-agents/mcp": {
975
- "version": "5.18.2",
976
- "resolved": "https://registry.npmjs.org/@lousy-agents/mcp/-/mcp-5.18.2.tgz",
977
- "integrity": "sha512-KGXiIqXT/9s3BN0W3Ag2p4kr0RPbVC3JgrItIr3iEmLsSku+rik6C8f9EXYarT3eud92aZNvrN26qJZYcg2BpA==",
975
+ "version": "5.19.0",
976
+ "resolved": "https://registry.npmjs.org/@lousy-agents/mcp/-/mcp-5.19.0.tgz",
977
+ "integrity": "sha512-3Hm79pTx2Gpis3vd1sJWfJ+CSIYUAqF6AuU0VCNwhGqfO9V34IskwDsmp/WPTy6F6Io1FUhm7be7G3x4/MWI/g==",
978
978
  "dev": true,
979
979
  "license": "BSD-2-Clause-Patent",
980
980
  "dependencies": {
@@ -5851,9 +5851,9 @@
5851
5851
  "optional": true
5852
5852
  },
5853
5853
  "node_modules/tsx": {
5854
- "version": "4.23.4",
5855
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.4.tgz",
5856
- "integrity": "sha512-ZiUQ8oT/KzN51mJUWPqARYqwFLFJZtGZipRkw1ynHMr9vy3eU77m5yfF3Gzm6meEg/beW+lUu3fHYgskTN2oVQ==",
5854
+ "version": "4.23.5",
5855
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.5.tgz",
5856
+ "integrity": "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==",
5857
5857
  "dev": true,
5858
5858
  "license": "MIT",
5859
5859
  "dependencies": {
@@ -29,14 +29,14 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@biomejs/biome": "2.5.3",
32
- "@lousy-agents/mcp": "5.18.2",
32
+ "@lousy-agents/mcp": "5.19.0",
33
33
  "@modelcontextprotocol/server-sequential-thinking": "2026.7.4",
34
34
  "@testcontainers/postgresql": "12.0.4",
35
35
  "@types/node": "24.13.3",
36
36
  "@upstash/context7-mcp": "3.2.5",
37
37
  "chance": "1.1.13",
38
38
  "testcontainers": "12.0.4",
39
- "tsx": "4.23.4",
39
+ "tsx": "4.23.5",
40
40
  "typescript": "7.0.2",
41
41
  "vitest": "4.1.10"
42
42
  }
@@ -20,12 +20,12 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@biomejs/biome": "2.5.3",
23
- "@lousy-agents/mcp": "5.18.2",
23
+ "@lousy-agents/mcp": "5.19.0",
24
24
  "@modelcontextprotocol/server-sequential-thinking": "2026.7.4",
25
25
  "@types/node": "24.13.3",
26
26
  "@upstash/context7-mcp": "3.2.5",
27
27
  "chance": "1.1.13",
28
- "tsx": "4.23.4",
28
+ "tsx": "4.23.5",
29
29
  "typescript": "7.0.2",
30
30
  "vitest": "4.1.10"
31
31
  }
package/dist/index.js CHANGED
@@ -3032,7 +3032,7 @@ exports.basename = (path, { windows } = {}) => {
3032
3032
 
3033
3033
 
3034
3034
  },
3035
- 4140(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
3035
+ 6567(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
3036
3036
  // NAMESPACE OBJECT: ../../node_modules/micromark/lib/constructs.js
3037
3037
  var constructs_namespaceObject = {};
3038
3038
  __webpack_require__.r(constructs_namespaceObject);
@@ -11689,11 +11689,11 @@ const $ZodXor = /*@__PURE__*/ (/* unused pure expression or super */ null && (co
11689
11689
  })));
11690
11690
  const $ZodDiscriminatedUnion =
11691
11691
  /*@__PURE__*/
11692
- (/* unused pure expression or super */ null && (core.$constructor("$ZodDiscriminatedUnion", (inst, def) => {
11692
+ $constructor("$ZodDiscriminatedUnion", (inst, def) => {
11693
11693
  def.inclusive = false;
11694
11694
  $ZodUnion.init(inst, def);
11695
11695
  const _super = inst._zod.parse;
11696
- util.defineLazy(inst._zod, "propValues", () => {
11696
+ defineLazy(inst._zod, "propValues", () => {
11697
11697
  const propValues = {};
11698
11698
  for (const option of def.options) {
11699
11699
  const pv = option._zod.propValues;
@@ -11709,7 +11709,7 @@ const $ZodDiscriminatedUnion =
11709
11709
  }
11710
11710
  return propValues;
11711
11711
  });
11712
- const disc = util.cached(() => {
11712
+ const disc = util_cached(() => {
11713
11713
  const opts = def.options;
11714
11714
  const map = new Map();
11715
11715
  for (const o of opts) {
@@ -11727,7 +11727,7 @@ const $ZodDiscriminatedUnion =
11727
11727
  });
11728
11728
  inst._zod.parse = (payload, ctx) => {
11729
11729
  const input = payload.value;
11730
- if (!util.isObject(input)) {
11730
+ if (!util_isObject(input)) {
11731
11731
  payload.issues.push({
11732
11732
  code: "invalid_type",
11733
11733
  expected: "object",
@@ -11760,7 +11760,7 @@ const $ZodDiscriminatedUnion =
11760
11760
  });
11761
11761
  return payload;
11762
11762
  };
11763
- })));
11763
+ });
11764
11764
  const $ZodIntersection = /*@__PURE__*/ $constructor("$ZodIntersection", (inst, def) => {
11765
11765
  $ZodType.init(inst, def);
11766
11766
  inst._zod.parse = (payload, ctx) => {
@@ -15937,17 +15937,17 @@ function xor(options, params) {
15937
15937
  ...util.normalizeParams(params),
15938
15938
  });
15939
15939
  }
15940
- const ZodDiscriminatedUnion = /*@__PURE__*/ (/* unused pure expression or super */ null && (core.$constructor("ZodDiscriminatedUnion", (inst, def) => {
15940
+ const ZodDiscriminatedUnion = /*@__PURE__*/ $constructor("ZodDiscriminatedUnion", (inst, def) => {
15941
15941
  ZodUnion.init(inst, def);
15942
- core.$ZodDiscriminatedUnion.init(inst, def);
15943
- })));
15942
+ $ZodDiscriminatedUnion.init(inst, def);
15943
+ });
15944
15944
  function discriminatedUnion(discriminator, options, params) {
15945
15945
  // const [options, params] = args;
15946
15946
  return new ZodDiscriminatedUnion({
15947
15947
  type: "union",
15948
15948
  options,
15949
15949
  discriminator,
15950
- ...util.normalizeParams(params),
15950
+ ...normalizeParams(params),
15951
15951
  });
15952
15952
  }
15953
15953
  const ZodIntersection = /*@__PURE__*/ $constructor("ZodIntersection", (inst, def) => {
@@ -33047,11 +33047,28 @@ async function walkMarkdownFiles(targetDir, resolvedRootDir, dir, depth, deriveN
33047
33047
 
33048
33048
  /** Maximum hook config file size: 1 MB */ const MAX_CONFIG_FILE_BYTES = 1_048_576;
33049
33049
  /** Matches the Copilot hook key `"preToolUse":` to detect hook section presence */ const COPILOT_HOOK_PATTERN = /"preToolUse"\s*:/;
33050
- /** Matches the Claude hook key `"PreToolUse":` to detect hook section presence */ const CLAUDE_HOOK_PATTERN = /"PreToolUse"\s*:/;
33050
+ /** Matches the Claude hook key `"hooks":` to detect hook section presence */ const CLAUDE_HOOK_PATTERN = /"hooks"\s*:/;
33051
33051
  /** Catalog hook config paths converted to OS-native relative paths. */ const HOOK_CONFIG_PATHS = hookConfigPaths().map((entry)=>({
33052
33052
  relativePath: (0,external_node_path_.join)(...entry.relativePath.split("/")),
33053
33053
  platform: entry.platform
33054
33054
  }));
33055
+ /**
33056
+ * Structural check for a `hooks` object on a parsed settings file.
33057
+ *
33058
+ * Uses `Object.hasOwn` so a `hooks` key inherited from the prototype chain
33059
+ * cannot make an unrelated file look like a hook config. Arrays are rejected
33060
+ * even though `typeof [] === "object"`, because a hooks section is keyed by
33061
+ * event name.
33062
+ */ function hasHooksSection(parsed) {
33063
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
33064
+ return false;
33065
+ }
33066
+ if (!Object.hasOwn(parsed, "hooks")) {
33067
+ return false;
33068
+ }
33069
+ const hooks = parsed.hooks;
33070
+ return hooks !== null && typeof hooks === "object" && !Array.isArray(hooks);
33071
+ }
33055
33072
  /**
33056
33073
  * File system implementation of the hook config lint gateway.
33057
33074
  */ class FileSystemHookConfigGateway {
@@ -33098,14 +33115,24 @@ async function walkMarkdownFiles(targetDir, resolvedRootDir, dir, depth, deriveN
33098
33115
  return readFileNoFollow(filePath, MAX_CONFIG_FILE_BYTES);
33099
33116
  }
33100
33117
  /**
33101
- * Lightweight heuristic to check if a file may contain a pre-tool-use hooks section.
33102
- * Uses substring search rather than JSON.parse so that files with invalid JSON are
33103
- * still discovered and surfaced as `hook/invalid-json` diagnostics by the use case.
33118
+ * Checks whether a file may contain a hooks section.
33119
+ *
33120
+ * Claude Code settings files carry much more than hooks, and the `hooks`
33121
+ * key is generic enough to appear inside unrelated string values, so a
33122
+ * parsed structural check is used when the JSON is well-formed. Malformed
33123
+ * JSON falls back to a substring search so the file is still discovered
33124
+ * and surfaced as a `hook/invalid-json` diagnostic by the use case.
33104
33125
  */ mayContainHookSection(content, platform) {
33105
33126
  if (platform === "copilot") {
33106
33127
  return COPILOT_HOOK_PATTERN.test(content);
33107
33128
  }
33108
- return CLAUDE_HOOK_PATTERN.test(content);
33129
+ let parsed;
33130
+ try {
33131
+ parsed = JSON.parse(content);
33132
+ } catch {
33133
+ return CLAUDE_HOOK_PATTERN.test(content);
33134
+ }
33135
+ return hasHooksSection(parsed);
33109
33136
  }
33110
33137
  }
33111
33138
  /**
@@ -52849,6 +52876,7 @@ async function resolveConfigPathOrNull(targetDir, relativePath) {
52849
52876
  "hook/invalid-json": "error",
52850
52877
  "hook/invalid-config": "error",
52851
52878
  "hook/missing-command": "error",
52879
+ "hook/unknown-event": "error",
52852
52880
  "hook/missing-matcher": "warn",
52853
52881
  "hook/missing-timeout": "warn"
52854
52882
  },
@@ -53153,6 +53181,167 @@ async function resolveConfigPathOrNull(targetDir, relativePath) {
53153
53181
  return lines[0]?.trim() === "---";
53154
53182
  }
53155
53183
 
53184
+ ;// CONCATENATED MODULE: ../core/src/entities/claude-hook-schema.ts
53185
+ // biome-ignore-all lint/style/useNamingConvention: Claude Code API uses PascalCase hook event names (PreToolUse) and snake_case handler types (mcp_tool)
53186
+ /**
53187
+ * Zod schemas for the Claude Code hooks configuration format.
53188
+ *
53189
+ * Lives in entities (Layer 1) so that use cases can import it without
53190
+ * violating the dependency rule, mirroring `copilot-hook-schema.ts`.
53191
+ *
53192
+ * Modelled on https://code.claude.com/docs/en/hooks. Claude Code settings
53193
+ * files carry far more than hooks, so the top level is permissive
53194
+ * (`.passthrough()`); the `hooks` section itself is strict so that a
53195
+ * misspelled event name — which silently never fires — is reported.
53196
+ */
53197
+ const MAX_HOOKS_PER_ENTRY = 100;
53198
+ /**
53199
+ * Events that accept a `matcher`. The remainder fire unconditionally, so a
53200
+ * missing `matcher` is only worth reporting for the events listed here.
53201
+ */ const MATCHER_SUPPORTING_EVENTS = new Set([
53202
+ "PreToolUse",
53203
+ "PostToolUse",
53204
+ "PostToolUseFailure",
53205
+ "PermissionRequest",
53206
+ "PermissionDenied",
53207
+ "SessionStart",
53208
+ "Setup",
53209
+ "SessionEnd",
53210
+ "Notification",
53211
+ "SubagentStart",
53212
+ "SubagentStop",
53213
+ "PreCompact",
53214
+ "PostCompact",
53215
+ "ConfigChange",
53216
+ "DirectoryAdded",
53217
+ "FileChanged",
53218
+ "StopFailure",
53219
+ "InstructionsLoaded",
53220
+ "UserPromptExpansion",
53221
+ "Elicitation",
53222
+ "ElicitationResult"
53223
+ ]);
53224
+ /** Fields every handler type accepts regardless of `type`. */ const sharedHandlerFields = {
53225
+ if: schemas_string().optional(),
53226
+ timeout: schemas_number().positive().optional(),
53227
+ statusMessage: schemas_string().optional(),
53228
+ once: schemas_boolean().optional()
53229
+ };
53230
+ /** Regex that allows standard env var names and rejects __proto__ (the prototype-polluting key). */ const ENV_KEY_PATTERN = /^(?!__proto__$)[a-zA-Z_][a-zA-Z0-9_]*$/;
53231
+ const ClaudeCommandHandlerSchema = schemas_object({
53232
+ ...sharedHandlerFields,
53233
+ type: schemas_literal("command"),
53234
+ command: schemas_string().min(1, "Hook command must not be empty"),
53235
+ args: schemas_array(schemas_string()).optional(),
53236
+ shell: schemas_enum([
53237
+ "bash",
53238
+ "powershell"
53239
+ ]).optional(),
53240
+ async: schemas_boolean().optional(),
53241
+ asyncRewake: schemas_boolean().optional()
53242
+ }).strict();
53243
+ const ClaudeHttpHandlerSchema = schemas_object({
53244
+ ...sharedHandlerFields,
53245
+ type: schemas_literal("http"),
53246
+ url: schemas_string().min(1, "Hook url must not be empty"),
53247
+ headers: schemas_record(schemas_string(), schemas_string()).optional(),
53248
+ allowedEnvVars: schemas_array(schemas_string().regex(ENV_KEY_PATTERN, "Hook allowedEnvVars entry must be a valid identifier (no prototype-polluting keys)")).optional()
53249
+ }).strict();
53250
+ const ClaudeMcpToolHandlerSchema = schemas_object({
53251
+ ...sharedHandlerFields,
53252
+ type: schemas_literal("mcp_tool"),
53253
+ server: schemas_string().min(1, "Hook server must not be empty"),
53254
+ tool: schemas_string().min(1, "Hook tool must not be empty"),
53255
+ input: schemas_record(schemas_string(), unknown()).optional()
53256
+ }).strict();
53257
+ const ClaudePromptHandlerSchema = schemas_object({
53258
+ ...sharedHandlerFields,
53259
+ type: schemas_literal("prompt"),
53260
+ prompt: schemas_string().min(1, "Hook prompt must not be empty"),
53261
+ model: schemas_string().optional()
53262
+ }).strict();
53263
+ const ClaudeAgentHandlerSchema = schemas_object({
53264
+ ...sharedHandlerFields,
53265
+ type: schemas_literal("agent"),
53266
+ prompt: schemas_string().min(1, "Hook prompt must not be empty"),
53267
+ model: schemas_string().optional()
53268
+ }).strict();
53269
+ /**
53270
+ * A single hook handler. Discriminated on `type` so that each handler is
53271
+ * validated against its own required fields — an `http` handler is not
53272
+ * expected to carry a `command`, and vice versa.
53273
+ */ const ClaudeHookHandlerSchema = discriminatedUnion("type", [
53274
+ ClaudeCommandHandlerSchema,
53275
+ ClaudeHttpHandlerSchema,
53276
+ ClaudeMcpToolHandlerSchema,
53277
+ ClaudePromptHandlerSchema,
53278
+ ClaudeAgentHandlerSchema
53279
+ ]);
53280
+ /**
53281
+ * A single entry within a hook event array.
53282
+ *
53283
+ * `matcher` is optional for every event: Claude Code treats an omitted
53284
+ * matcher as "match all", and many events accept no matcher at all.
53285
+ */ const ClaudeHookEntrySchema = schemas_object({
53286
+ matcher: schemas_string().optional(),
53287
+ hooks: schemas_array(ClaudeHookHandlerSchema).min(1, "Hook entry must include at least one handler").max(MAX_HOOKS_PER_ENTRY)
53288
+ }).strict();
53289
+ const hookEventArray = schemas_array(ClaudeHookEntrySchema);
53290
+ /**
53291
+ * Every hook event Claude Code dispatches, each optional so that configs may
53292
+ * use any combination.
53293
+ *
53294
+ * Spelled out rather than generated from a list so the schema keeps precise
53295
+ * inferred types without a type assertion.
53296
+ */ const claudeHookEventShape = {
53297
+ SessionStart: hookEventArray.optional(),
53298
+ Setup: hookEventArray.optional(),
53299
+ UserPromptSubmit: hookEventArray.optional(),
53300
+ UserPromptExpansion: hookEventArray.optional(),
53301
+ PreToolUse: hookEventArray.optional(),
53302
+ PermissionRequest: hookEventArray.optional(),
53303
+ PermissionDenied: hookEventArray.optional(),
53304
+ PostToolUse: hookEventArray.optional(),
53305
+ PostToolUseFailure: hookEventArray.optional(),
53306
+ PostToolBatch: hookEventArray.optional(),
53307
+ Notification: hookEventArray.optional(),
53308
+ MessageDisplay: hookEventArray.optional(),
53309
+ SubagentStart: hookEventArray.optional(),
53310
+ SubagentStop: hookEventArray.optional(),
53311
+ TaskCreated: hookEventArray.optional(),
53312
+ TaskCompleted: hookEventArray.optional(),
53313
+ Stop: hookEventArray.optional(),
53314
+ StopFailure: hookEventArray.optional(),
53315
+ TeammateIdle: hookEventArray.optional(),
53316
+ InstructionsLoaded: hookEventArray.optional(),
53317
+ ConfigChange: hookEventArray.optional(),
53318
+ CwdChanged: hookEventArray.optional(),
53319
+ DirectoryAdded: hookEventArray.optional(),
53320
+ FileChanged: hookEventArray.optional(),
53321
+ WorktreeCreate: hookEventArray.optional(),
53322
+ WorktreeRemove: hookEventArray.optional(),
53323
+ PreCompact: hookEventArray.optional(),
53324
+ PostCompact: hookEventArray.optional(),
53325
+ Elicitation: hookEventArray.optional(),
53326
+ ElicitationResult: hookEventArray.optional(),
53327
+ SessionEnd: hookEventArray.optional()
53328
+ };
53329
+ /**
53330
+ * Every known hook event name.
53331
+ *
53332
+ * Derived from the schema shape so the two can never drift apart, and used to
53333
+ * report the valid set when an unknown event name is encountered.
53334
+ */ const CLAUDE_HOOK_EVENTS = Object.keys(claudeHookEventShape);
53335
+ /**
53336
+ * Zod schema for the hooks section of a Claude Code settings file.
53337
+ *
53338
+ * Every event is optional — configs may use any combination — but unknown
53339
+ * event names are rejected so that typos surface instead of silently
53340
+ * never firing.
53341
+ */ const ClaudeHooksConfigSchema = schemas_object({
53342
+ hooks: schemas_object(claudeHookEventShape).strict()
53343
+ }).passthrough();
53344
+
53156
53345
  ;// CONCATENATED MODULE: ../core/src/entities/copilot-hook-schema.ts
53157
53346
  /**
53158
53347
  * Zod schemas for the GitHub Copilot hooks configuration format.
@@ -53164,7 +53353,7 @@ async function resolveConfigPathOrNull(targetDir, relativePath) {
53164
53353
  * depend on @lousy-agents/core.
53165
53354
  */
53166
53355
  const MAX_HOOKS_PER_EVENT = 100;
53167
- /** Regex that allows standard env var names and rejects __proto__ (the prototype-polluting key). */ const ENV_KEY_PATTERN = /^(?!__proto__$)[a-zA-Z_][a-zA-Z0-9_]*$/;
53356
+ /** Regex that allows standard env var names and rejects __proto__ (the prototype-polluting key). */ const copilot_hook_schema_ENV_KEY_PATTERN = /^(?!__proto__$)[a-zA-Z_][a-zA-Z0-9_]*$/;
53168
53357
  /**
53169
53358
  * Zod schema for a single GitHub Copilot hook command entry.
53170
53359
  */ const CopilotHookCommandSchema = schemas_object({
@@ -53173,7 +53362,7 @@ const MAX_HOOKS_PER_EVENT = 100;
53173
53362
  powershell: schemas_string().min(1, "Hook PowerShell command must not be empty").optional(),
53174
53363
  cwd: schemas_string().optional(),
53175
53364
  timeoutSec: schemas_number().positive().optional(),
53176
- env: schemas_record(schemas_string().regex(ENV_KEY_PATTERN, "Hook env key must be a valid identifier (no prototype-polluting keys)"), schemas_string()).optional()
53365
+ env: schemas_record(schemas_string().regex(copilot_hook_schema_ENV_KEY_PATTERN, "Hook env key must be a valid identifier (no prototype-polluting keys)"), schemas_string()).optional()
53177
53366
  }).strict().refine((data)=>Boolean(data.bash) || Boolean(data.powershell), {
53178
53367
  message: "At least one of 'bash' or 'powershell' must be provided and non-empty"
53179
53368
  });
@@ -53193,33 +53382,78 @@ const hookArray = schemas_array(CopilotHookCommandSchema).max(MAX_HOOKS_PER_EVEN
53193
53382
  }).strict();
53194
53383
 
53195
53384
  ;// CONCATENATED MODULE: ../core/src/use-cases/lint-hook-config.ts
53196
- // biome-ignore-all lint/style/useNamingConvention: Claude Code API uses PascalCase hook event names (PreToolUse)
53197
53385
  /**
53198
- * Use case for linting pre-tool-use hook configurations.
53386
+ * Use case for linting hook configurations.
53199
53387
  * Validates GitHub Copilot and Claude Code hook config files.
53200
53388
  */
53201
53389
 
53202
53390
 
53203
53391
  const INVALID_JSON_MESSAGE_PREFIX = "Invalid JSON in hook configuration file";
53392
+ /** Joins a Zod issue path into the dotted `field` shown on a diagnostic. */ function toFieldPath(path) {
53393
+ return path.length > 0 ? path.join(".") : undefined;
53394
+ }
53204
53395
  /**
53205
- * Zod schema for a single Claude Code hook command entry.
53206
- */ const ClaudeHookCommandSchema = schemas_object({
53207
- type: schemas_literal("command"),
53208
- command: schemas_string().min(1, "Hook command must not be empty")
53209
- }).strict();
53210
- /**
53211
- * Zod schema for a single Claude Code PreToolUse hook entry.
53212
- */ const ClaudePreToolUseEntrySchema = schemas_object({
53213
- matcher: schemas_string().optional(),
53214
- hooks: schemas_array(ClaudeHookCommandSchema).min(1)
53215
- }).strict();
53396
+ * Expands a single Zod issue into diagnostics.
53397
+ *
53398
+ * `unrecognized_keys` issues carry every offending key on one issue whose path
53399
+ * points at the *containing* object, so they are expanded into one diagnostic
53400
+ * per key. A stray key directly under `hooks` is a misspelled event name —
53401
+ * the most valuable defect this linter can catch, since Claude Code silently
53402
+ * never fires such a hook.
53403
+ */ function toClaudeDiagnostics(issue) {
53404
+ if (issue.code === "unrecognized_keys") {
53405
+ const isEventName = issue.path.length === 1 && issue.path[0] === "hooks";
53406
+ return issue.keys.map((key)=>({
53407
+ line: 1,
53408
+ severity: "error",
53409
+ message: isEventName ? `Unknown hook event '${key}'. A misspelled event never fires. Valid events: ${CLAUDE_HOOK_EVENTS.join(", ")}` : `Unrecognized key: "${key}"`,
53410
+ field: [
53411
+ ...issue.path,
53412
+ key
53413
+ ].join("."),
53414
+ ruleId: isEventName ? "hook/unknown-event" : "hook/invalid-config"
53415
+ }));
53416
+ }
53417
+ const lastPathSegment = issue.path.at(-1);
53418
+ const isMissingCommand = lastPathSegment === "command" && (issue.code === "too_small" || issue.code === "invalid_type");
53419
+ return [
53420
+ {
53421
+ line: 1,
53422
+ severity: "error",
53423
+ message: issue.message,
53424
+ field: toFieldPath([
53425
+ ...issue.path
53426
+ ]),
53427
+ ruleId: isMissingCommand ? "hook/missing-command" : "hook/invalid-config"
53428
+ }
53429
+ ];
53430
+ }
53216
53431
  /**
53217
- * Zod schema for the Claude Code hooks section within settings.
53218
- */ const ClaudeHooksConfigSchema = schemas_object({
53219
- hooks: schemas_object({
53220
- PreToolUse: schemas_array(ClaudePreToolUseEntrySchema).min(1)
53221
- }).passthrough()
53222
- }).passthrough();
53432
+ * Warns when an entry omits `matcher` for an event that supports one.
53433
+ *
53434
+ * Events that accept no matcher (`Stop`, `UserPromptSubmit`, …) are skipped:
53435
+ * an omitted matcher there is the only valid spelling, not an oversight.
53436
+ */ function collectMissingMatcherWarnings(hooks) {
53437
+ const diagnostics = [];
53438
+ for (const event of CLAUDE_HOOK_EVENTS){
53439
+ if (!MATCHER_SUPPORTING_EVENTS.has(event)) {
53440
+ continue;
53441
+ }
53442
+ const entries = hooks[event] ?? [];
53443
+ entries.forEach((entry, index)=>{
53444
+ if (entry.matcher === undefined) {
53445
+ diagnostics.push({
53446
+ line: 1,
53447
+ severity: "warning",
53448
+ message: `Recommended field 'matcher' is missing from ${event} hook entry. Without a matcher, the hook runs for every occurrence.`,
53449
+ field: `hooks.${event}[${index}].matcher`,
53450
+ ruleId: "hook/missing-matcher"
53451
+ });
53452
+ }
53453
+ });
53454
+ }
53455
+ return diagnostics;
53456
+ }
53223
53457
  /**
53224
53458
  * Use case for linting hook configuration files across a repository.
53225
53459
  */ class LintHookConfigUseCase {
@@ -53321,34 +53555,11 @@ const INVALID_JSON_MESSAGE_PREFIX = "Invalid JSON in hook configuration file";
53321
53555
  return diagnostics;
53322
53556
  }
53323
53557
  validateClaudeConfig(parsed) {
53324
- const diagnostics = [];
53325
53558
  const result = ClaudeHooksConfigSchema.safeParse(parsed);
53326
53559
  if (!result.success) {
53327
- for (const issue of result.error.issues){
53328
- const lastPathSegment = issue.path.length > 0 ? issue.path[issue.path.length - 1] : undefined;
53329
- const isMissingCommand = lastPathSegment === "command" && (issue.code === "too_small" || issue.code === "invalid_type");
53330
- diagnostics.push({
53331
- line: 1,
53332
- severity: "error",
53333
- message: issue.message,
53334
- field: issue.path.length > 0 ? issue.path.join(".") : undefined,
53335
- ruleId: isMissingCommand ? "hook/missing-command" : "hook/invalid-config"
53336
- });
53337
- }
53338
- return diagnostics;
53560
+ return result.error.issues.flatMap((issue)=>toClaudeDiagnostics(issue));
53339
53561
  }
53340
- for (const [index, entry] of result.data.hooks.PreToolUse.entries()){
53341
- if (entry.matcher === undefined) {
53342
- diagnostics.push({
53343
- line: 1,
53344
- severity: "warning",
53345
- message: "Recommended field 'matcher' is missing from PreToolUse hook entry. Without a matcher, the hook runs for all tools.",
53346
- field: `hooks.PreToolUse[${index}].matcher`,
53347
- ruleId: "hook/missing-matcher"
53348
- });
53349
- }
53350
- }
53351
- return diagnostics;
53562
+ return collectMissingMatcherWarnings(result.data.hooks);
53352
53563
  }
53353
53564
  }
53354
53565
 
@@ -63711,4 +63922,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
63711
63922
  // module factories are used so entry inlining is disabled
63712
63923
  // startup
63713
63924
  // Load entry module and return exports
63714
- var __webpack_exports__ = __webpack_require__(4140);
63925
+ var __webpack_exports__ = __webpack_require__(6567);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/cli",
3
- "version": "5.19.0",
3
+ "version": "5.19.1",
4
4
  "description": "CLI scaffolding tool that sets up projects with structure, instructions, and feedback loops for AI coding assistants",
5
5
  "type": "module",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "devDependencies": {
24
24
  "@biomejs/biome": "2.5.3",
25
25
  "@eslint/eslintrc": "3.3.6",
26
- "@lousy-agents/mcp": "5.18.2",
26
+ "@lousy-agents/mcp": "5.19.0",
27
27
  "@modelcontextprotocol/server-sequential-thinking": "2026.7.4",
28
28
  "@testing-library/jest-dom": "7.0.0",
29
29
  "@testing-library/react": "16.3.2",