@lousy-agents/cli 5.6.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -32244,11 +32244,11 @@ var external_node_tty_namespaceObject = /*#__PURE__*/__webpack_require__.t(exter
32244
32244
  const {
32245
32245
  env: consola_DXBYu_KD_env = {},
32246
32246
  argv = [],
32247
- platform = ""
32247
+ platform: consola_DXBYu_KD_platform = ""
32248
32248
  } = typeof process === "undefined" ? {} : process;
32249
32249
  const isDisabled = "NO_COLOR" in consola_DXBYu_KD_env || argv.includes("--no-color");
32250
32250
  const isForced = "FORCE_COLOR" in consola_DXBYu_KD_env || argv.includes("--color");
32251
- const consola_DXBYu_KD_isWindows = platform === "win32";
32251
+ const consola_DXBYu_KD_isWindows = consola_DXBYu_KD_platform === "win32";
32252
32252
  const isDumbTerminal = consola_DXBYu_KD_env.TERM === "dumb";
32253
32253
  const isCompatibleTerminal = external_node_tty_namespaceObject && external_node_tty_.isatty && external_node_tty_.isatty(1) && consola_DXBYu_KD_env.TERM && !isDumbTerminal;
32254
32254
  const isCI = "CI" in consola_DXBYu_KD_env && ("GITHUB_ACTIONS" in consola_DXBYu_KD_env || "GITLAB_CI" in consola_DXBYu_KD_env || "CIRCLECI" in consola_DXBYu_KD_env);
@@ -36673,6 +36673,91 @@ const initCommand = defineCommand({
36673
36673
  return new FileSystemAgentLintGateway();
36674
36674
  }
36675
36675
 
36676
+ ;// CONCATENATED MODULE: ../core/src/gateways/hook-config-gateway.ts
36677
+ /**
36678
+ * Gateway for hook configuration file system operations.
36679
+ * Discovers hook config files for GitHub Copilot and Claude Code.
36680
+ */
36681
+
36682
+
36683
+ /** Maximum hook config file size: 1 MB */ const MAX_CONFIG_FILE_BYTES = 1_048_576;
36684
+ /** Matches the Copilot hook key `"preToolUse":` to detect hook section presence */ const COPILOT_HOOK_PATTERN = /"preToolUse"\s*:/;
36685
+ /** Matches the Claude hook key `"PreToolUse":` to detect hook section presence */ const CLAUDE_HOOK_PATTERN = /"PreToolUse"\s*:/;
36686
+ /**
36687
+ * Hook configuration file locations to search.
36688
+ */ const HOOK_CONFIG_PATHS = [
36689
+ {
36690
+ relativePath: (0,external_node_path_.join)(".github", "copilot", "hooks.json"),
36691
+ platform: "copilot"
36692
+ },
36693
+ {
36694
+ relativePath: (0,external_node_path_.join)(".claude", "settings.json"),
36695
+ platform: "claude"
36696
+ },
36697
+ {
36698
+ relativePath: (0,external_node_path_.join)(".claude", "settings.local.json"),
36699
+ platform: "claude"
36700
+ }
36701
+ ];
36702
+ /**
36703
+ * File system implementation of the hook config lint gateway.
36704
+ */ class FileSystemHookConfigGateway {
36705
+ async discoverHookFiles(targetDir) {
36706
+ const discovered = [];
36707
+ for (const config of HOOK_CONFIG_PATHS){
36708
+ let safePath;
36709
+ try {
36710
+ safePath = await file_system_utils_resolveSafePath(targetDir, config.relativePath);
36711
+ } catch {
36712
+ continue;
36713
+ }
36714
+ if (!await file_system_utils_fileExists(safePath)) {
36715
+ continue;
36716
+ }
36717
+ const stats = await (0,promises_.lstat)(safePath);
36718
+ if (stats.isSymbolicLink()) {
36719
+ continue;
36720
+ }
36721
+ try {
36722
+ await assertFileSizeWithinLimit(safePath, MAX_CONFIG_FILE_BYTES, `Hook config ${config.relativePath}`);
36723
+ } catch {
36724
+ continue;
36725
+ }
36726
+ const content = await (0,promises_.readFile)(safePath, "utf-8");
36727
+ if (this.mayContainHookSection(content, config.platform)) {
36728
+ discovered.push({
36729
+ filePath: safePath,
36730
+ platform: config.platform
36731
+ });
36732
+ }
36733
+ }
36734
+ return discovered;
36735
+ }
36736
+ async readFileContent(filePath) {
36737
+ const stats = await (0,promises_.lstat)(filePath);
36738
+ if (stats.isSymbolicLink()) {
36739
+ throw new Error(`Symlinks are not allowed: ${filePath}`);
36740
+ }
36741
+ await assertFileSizeWithinLimit(filePath, MAX_CONFIG_FILE_BYTES, `Hook config ${filePath}`);
36742
+ return (0,promises_.readFile)(filePath, "utf-8");
36743
+ }
36744
+ /**
36745
+ * Lightweight heuristic to check if a file may contain a pre-tool-use hooks section.
36746
+ * Uses substring search rather than JSON.parse so that files with invalid JSON are
36747
+ * still discovered and surfaced as `hook/invalid-json` diagnostics by the use case.
36748
+ */ mayContainHookSection(content, platform) {
36749
+ if (platform === "copilot") {
36750
+ return COPILOT_HOOK_PATTERN.test(content);
36751
+ }
36752
+ return CLAUDE_HOOK_PATTERN.test(content);
36753
+ }
36754
+ }
36755
+ /**
36756
+ * Creates and returns the default hook config lint gateway.
36757
+ */ function createHookConfigGateway() {
36758
+ return new FileSystemHookConfigGateway();
36759
+ }
36760
+
36676
36761
  ;// CONCATENATED MODULE: ../core/src/gateways/instruction-file-discovery-gateway.ts
36677
36762
  /**
36678
36763
  * Gateway for discovering instruction files across multiple formats.
@@ -55723,6 +55808,13 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55723
55808
  "agent/invalid-description": "error",
55724
55809
  "agent/invalid-field": "warn"
55725
55810
  },
55811
+ hooks: {
55812
+ "hook/invalid-json": "error",
55813
+ "hook/invalid-config": "error",
55814
+ "hook/missing-command": "error",
55815
+ "hook/missing-matcher": "warn",
55816
+ "hook/missing-timeout": "warn"
55817
+ },
55726
55818
  instructions: {
55727
55819
  "instruction/parse-error": "warn",
55728
55820
  "instruction/command-not-in-code-block": "warn",
@@ -55755,6 +55847,7 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55755
55847
  ]));
55756
55848
  /** Zod schema for the lint.rules section of the config */ const LintRulesConfigSchema = schemas_object({
55757
55849
  agents: RuleConfigMapSchema.optional(),
55850
+ hooks: RuleConfigMapSchema.optional(),
55758
55851
  instructions: RuleConfigMapSchema.optional(),
55759
55852
  skills: RuleConfigMapSchema.optional()
55760
55853
  });
@@ -55799,6 +55892,7 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55799
55892
  }
55800
55893
  return {
55801
55894
  agents: mergeTargetRules(DEFAULT_LINT_RULES.agents, rules.agents),
55895
+ hooks: mergeTargetRules(DEFAULT_LINT_RULES.hooks, rules.hooks),
55802
55896
  instructions: mergeTargetRules(DEFAULT_LINT_RULES.instructions, rules.instructions),
55803
55897
  skills: mergeTargetRules(DEFAULT_LINT_RULES.skills, rules.skills)
55804
55898
  };
@@ -56190,6 +56284,7 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
56190
56284
  */ /** Maps a lint target to its config key */ const TARGET_TO_CONFIG_KEY = {
56191
56285
  skill: "skills",
56192
56286
  agent: "agents",
56287
+ hook: "hooks",
56193
56288
  instruction: "instructions"
56194
56289
  };
56195
56290
  /**
@@ -56391,6 +56486,205 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
56391
56486
  return lines[0]?.trim() === "---";
56392
56487
  }
56393
56488
 
56489
+ ;// CONCATENATED MODULE: ../core/src/entities/copilot-hook-schema.ts
56490
+ /**
56491
+ * Zod schemas for the GitHub Copilot hooks configuration format.
56492
+ *
56493
+ * Lives in entities (Layer 1) so that use cases can import it without
56494
+ * violating the dependency rule. The agent-shell package maintains an
56495
+ * aligned copy (packages/agent-shell/src/types.ts HooksConfigSchema)
56496
+ * because agent-shell is a standalone published binary that cannot
56497
+ * depend on @lousy-agents/core.
56498
+ */
56499
+ const MAX_HOOKS_PER_EVENT = 100;
56500
+ /** 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_]*$/;
56501
+ /**
56502
+ * Zod schema for a single GitHub Copilot hook command entry.
56503
+ */ const CopilotHookCommandSchema = schemas_object({
56504
+ type: schemas_literal("command"),
56505
+ bash: schemas_string().min(1, "Hook bash command must not be empty").optional(),
56506
+ powershell: schemas_string().min(1, "Hook PowerShell command must not be empty").optional(),
56507
+ cwd: schemas_string().optional(),
56508
+ timeoutSec: schemas_number().positive().optional(),
56509
+ env: record(schemas_string().regex(ENV_KEY_PATTERN, "Hook env key must be a valid identifier (no prototype-polluting keys)"), schemas_string()).optional()
56510
+ }).strict().refine((data)=>Boolean(data.bash) || Boolean(data.powershell), {
56511
+ message: "At least one of 'bash' or 'powershell' must be provided and non-empty"
56512
+ });
56513
+ const hookArray = schemas_array(CopilotHookCommandSchema).max(MAX_HOOKS_PER_EVENT);
56514
+ /**
56515
+ * Zod schema for the GitHub Copilot hooks configuration file.
56516
+ * All hook event arrays are optional — configs may use any combination of events.
56517
+ */ const CopilotHooksConfigSchema = schemas_object({
56518
+ version: schemas_literal(1),
56519
+ hooks: schemas_object({
56520
+ sessionStart: hookArray.optional(),
56521
+ userPromptSubmitted: hookArray.optional(),
56522
+ preToolUse: hookArray.optional(),
56523
+ postToolUse: hookArray.optional(),
56524
+ sessionEnd: hookArray.optional()
56525
+ }).strict()
56526
+ }).strict();
56527
+
56528
+ ;// CONCATENATED MODULE: ../core/src/use-cases/lint-hook-config.ts
56529
+ // biome-ignore-all lint/style/useNamingConvention: Claude Code API uses PascalCase hook event names (PreToolUse)
56530
+ /**
56531
+ * Use case for linting pre-tool-use hook configurations.
56532
+ * Validates GitHub Copilot and Claude Code hook config files.
56533
+ */
56534
+
56535
+
56536
+ const INVALID_JSON_MESSAGE_PREFIX = "Invalid JSON in hook configuration file";
56537
+ /**
56538
+ * Zod schema for a single Claude Code hook command entry.
56539
+ */ const ClaudeHookCommandSchema = schemas_object({
56540
+ type: schemas_literal("command"),
56541
+ command: schemas_string().min(1, "Hook command must not be empty")
56542
+ }).strict();
56543
+ /**
56544
+ * Zod schema for a single Claude Code PreToolUse hook entry.
56545
+ */ const ClaudePreToolUseEntrySchema = schemas_object({
56546
+ matcher: schemas_string().optional(),
56547
+ hooks: schemas_array(ClaudeHookCommandSchema).min(1)
56548
+ }).strict();
56549
+ /**
56550
+ * Zod schema for the Claude Code hooks section within settings.
56551
+ */ const ClaudeHooksConfigSchema = schemas_object({
56552
+ hooks: schemas_object({
56553
+ PreToolUse: schemas_array(ClaudePreToolUseEntrySchema).min(1)
56554
+ }).passthrough()
56555
+ }).passthrough();
56556
+ /**
56557
+ * Use case for linting hook configuration files across a repository.
56558
+ */ class LintHookConfigUseCase {
56559
+ gateway;
56560
+ constructor(gateway){
56561
+ this.gateway = gateway;
56562
+ }
56563
+ async execute(input) {
56564
+ if (!input.targetDir) {
56565
+ throw new Error("Target directory is required");
56566
+ }
56567
+ const hookFiles = await this.gateway.discoverHookFiles(input.targetDir);
56568
+ const results = [];
56569
+ for (const hookFile of hookFiles){
56570
+ const content = await this.gateway.readFileContent(hookFile.filePath);
56571
+ const result = this.lintHookFile(hookFile, content);
56572
+ results.push(result);
56573
+ }
56574
+ const totalErrors = results.reduce((sum, r)=>sum + r.diagnostics.filter((d)=>d.severity === "error").length, 0);
56575
+ const totalWarnings = results.reduce((sum, r)=>sum + r.diagnostics.filter((d)=>d.severity === "warning").length, 0);
56576
+ return {
56577
+ results,
56578
+ totalFiles: hookFiles.length,
56579
+ totalErrors,
56580
+ totalWarnings
56581
+ };
56582
+ }
56583
+ lintHookFile(hookFile, content) {
56584
+ let parsed;
56585
+ try {
56586
+ parsed = JSON.parse(content);
56587
+ } catch (error) {
56588
+ const errorMessage = error instanceof Error && error.message ? `${INVALID_JSON_MESSAGE_PREFIX}: ${error.message}` : `${INVALID_JSON_MESSAGE_PREFIX}.`;
56589
+ return {
56590
+ filePath: hookFile.filePath,
56591
+ platform: hookFile.platform,
56592
+ diagnostics: [
56593
+ {
56594
+ line: 1,
56595
+ severity: "error",
56596
+ message: errorMessage,
56597
+ ruleId: "hook/invalid-json"
56598
+ }
56599
+ ],
56600
+ valid: false
56601
+ };
56602
+ }
56603
+ const diagnostics = hookFile.platform === "copilot" ? this.validateCopilotConfig(parsed) : this.validateClaudeConfig(parsed);
56604
+ return {
56605
+ filePath: hookFile.filePath,
56606
+ platform: hookFile.platform,
56607
+ diagnostics,
56608
+ valid: diagnostics.every((d)=>d.severity !== "error")
56609
+ };
56610
+ }
56611
+ validateCopilotConfig(parsed) {
56612
+ const diagnostics = [];
56613
+ const result = CopilotHooksConfigSchema.safeParse(parsed);
56614
+ if (!result.success) {
56615
+ for (const issue of result.error.issues){
56616
+ const lastPathSegment = issue.path.length > 0 ? issue.path[issue.path.length - 1] : undefined;
56617
+ const isCommandField = lastPathSegment === "bash" || lastPathSegment === "powershell";
56618
+ const isMissingCommand = // Refine failure: neither bash nor powershell provided.
56619
+ // Keyed off code===custom at the command-object level — the last
56620
+ // path segment is an array index (number), not a named field.
56621
+ issue.code === "custom" && !isCommandField || // Field-level failure: bash/powershell present but empty or wrong type
56622
+ isCommandField && (issue.code === "too_small" || issue.code === "invalid_type");
56623
+ diagnostics.push({
56624
+ line: 1,
56625
+ severity: "error",
56626
+ message: issue.message,
56627
+ field: issue.path.length > 0 ? issue.path.join(".") : undefined,
56628
+ ruleId: isMissingCommand ? "hook/missing-command" : "hook/invalid-config"
56629
+ });
56630
+ }
56631
+ return diagnostics;
56632
+ }
56633
+ const lifecycleNames = [
56634
+ "sessionStart",
56635
+ "userPromptSubmitted",
56636
+ "preToolUse",
56637
+ "postToolUse",
56638
+ "sessionEnd"
56639
+ ];
56640
+ for (const lifecycleName of lifecycleNames){
56641
+ const hooksForLifecycle = result.data.hooks[lifecycleName] ?? [];
56642
+ hooksForLifecycle.forEach((hook, index)=>{
56643
+ if (hook.timeoutSec === undefined) {
56644
+ diagnostics.push({
56645
+ line: 1,
56646
+ severity: "warning",
56647
+ message: "Recommended field 'timeoutSec' is missing from hook command",
56648
+ field: `hooks.${lifecycleName}[${index}].timeoutSec`,
56649
+ ruleId: "hook/missing-timeout"
56650
+ });
56651
+ }
56652
+ });
56653
+ }
56654
+ return diagnostics;
56655
+ }
56656
+ validateClaudeConfig(parsed) {
56657
+ const diagnostics = [];
56658
+ const result = ClaudeHooksConfigSchema.safeParse(parsed);
56659
+ if (!result.success) {
56660
+ for (const issue of result.error.issues){
56661
+ const lastPathSegment = issue.path.length > 0 ? issue.path[issue.path.length - 1] : undefined;
56662
+ const isMissingCommand = lastPathSegment === "command" && (issue.code === "too_small" || issue.code === "invalid_type");
56663
+ diagnostics.push({
56664
+ line: 1,
56665
+ severity: "error",
56666
+ message: issue.message,
56667
+ field: issue.path.length > 0 ? issue.path.join(".") : undefined,
56668
+ ruleId: isMissingCommand ? "hook/missing-command" : "hook/invalid-config"
56669
+ });
56670
+ }
56671
+ return diagnostics;
56672
+ }
56673
+ for (const [index, entry] of result.data.hooks.PreToolUse.entries()){
56674
+ if (entry.matcher === undefined) {
56675
+ diagnostics.push({
56676
+ line: 1,
56677
+ severity: "warning",
56678
+ message: "Recommended field 'matcher' is missing from PreToolUse hook entry. Without a matcher, the hook runs for all tools.",
56679
+ field: `hooks.PreToolUse[${index}].matcher`,
56680
+ ruleId: "hook/missing-matcher"
56681
+ });
56682
+ }
56683
+ }
56684
+ return diagnostics;
56685
+ }
56686
+ }
56687
+
56394
56688
  ;// CONCATENATED MODULE: ../core/src/use-cases/lint-skill-frontmatter.ts
56395
56689
 
56396
56690
  const AgentSkillFrontmatterSchema = schemas_object({
@@ -56562,6 +56856,8 @@ function hasFrontmatterDelimiters(content) {
56562
56856
 
56563
56857
 
56564
56858
 
56859
+
56860
+
56565
56861
  /** Schema for validating target directory */ const TargetDirSchema = schemas_string().min(1, "Target directory is required");
56566
56862
  /**
56567
56863
  * Validates the target directory.
@@ -56682,6 +56978,45 @@ function hasFrontmatterDelimiters(content) {
56682
56978
  });
56683
56979
  return agentOutputToLintOutput(output);
56684
56980
  }
56981
+ /**
56982
+ * Converts hook lint output to unified LintOutput.
56983
+ */ function hookOutputToLintOutput(output) {
56984
+ const diagnostics = [];
56985
+ for (const result of output.results){
56986
+ for (const d of result.diagnostics){
56987
+ diagnostics.push({
56988
+ filePath: result.filePath,
56989
+ line: d.line,
56990
+ severity: d.severity,
56991
+ message: d.message,
56992
+ field: d.field,
56993
+ ruleId: d.ruleId,
56994
+ target: "hook"
56995
+ });
56996
+ }
56997
+ }
56998
+ return {
56999
+ diagnostics,
57000
+ target: "hook",
57001
+ filesAnalyzed: output.results.map((r)=>r.filePath),
57002
+ summary: {
57003
+ totalFiles: output.totalFiles,
57004
+ totalErrors: output.totalErrors,
57005
+ totalWarnings: output.totalWarnings,
57006
+ totalInfos: 0
57007
+ }
57008
+ };
57009
+ }
57010
+ /**
57011
+ * Runs hook configuration linting.
57012
+ */ async function lintHooks(targetDir) {
57013
+ const gateway = createHookConfigGateway();
57014
+ const useCase = new LintHookConfigUseCase(gateway);
57015
+ const output = await useCase.execute({
57016
+ targetDir
57017
+ });
57018
+ return hookOutputToLintOutput(output);
57019
+ }
56685
57020
  /**
56686
57021
  * Runs instruction quality analysis.
56687
57022
  */ async function lintInstructions(targetDir) {
@@ -56731,7 +57066,7 @@ function hasFrontmatterDelimiters(content) {
56731
57066
  */ const lintCommand = defineCommand({
56732
57067
  meta: {
56733
57068
  name: "lint",
56734
- description: "Lint agent skills, custom agents, and instruction files. Validates frontmatter and instruction quality."
57069
+ description: "Lint agent skills, custom agents, instruction files, and hook configurations. Validates frontmatter, instruction quality, and hook config schemas."
56735
57070
  },
56736
57071
  args: {
56737
57072
  skills: {
@@ -56744,6 +57079,11 @@ function hasFrontmatterDelimiters(content) {
56744
57079
  description: "Lint custom agent frontmatter in .github/agents/",
56745
57080
  default: false
56746
57081
  },
57082
+ hooks: {
57083
+ type: "boolean",
57084
+ description: "Lint pre-tool-use hook configurations in .github/copilot/hooks.json, .claude/settings.json, and .claude/settings.local.json",
57085
+ default: false
57086
+ },
56747
57087
  instructions: {
56748
57088
  type: "boolean",
56749
57089
  description: "Analyze instruction quality across all instruction file formats",
@@ -56769,8 +57109,9 @@ function hasFrontmatterDelimiters(content) {
56769
57109
  }
56770
57110
  const lintSkillsFlag = context.args?.skills === true || context.data?.skills === true;
56771
57111
  const lintAgentsFlag = context.args?.agents === true || context.data?.agents === true;
57112
+ const lintHooksFlag = context.args?.hooks === true || context.data?.hooks === true;
56772
57113
  const lintInstructionsFlag = context.args?.instructions === true || context.data?.instructions === true;
56773
- const noFlagProvided = !lintSkillsFlag && !lintAgentsFlag && !lintInstructionsFlag;
57114
+ const noFlagProvided = !lintSkillsFlag && !lintAgentsFlag && !lintHooksFlag && !lintInstructionsFlag;
56774
57115
  const formatValue = context.args?.format ?? context.data?.format ?? "human";
56775
57116
  const format = [
56776
57117
  "human",
@@ -56794,6 +57135,13 @@ function hasFrontmatterDelimiters(content) {
56794
57135
  totalErrors += agentOutput.summary.totalErrors;
56795
57136
  totalWarnings += agentOutput.summary.totalWarnings;
56796
57137
  }
57138
+ if (noFlagProvided || lintHooksFlag) {
57139
+ const rawOutput = await lintHooks(targetDir);
57140
+ const hookOutput = applySeverityFilter(rawOutput, rulesConfig);
57141
+ allOutputs.push(hookOutput);
57142
+ totalErrors += hookOutput.summary.totalErrors;
57143
+ totalWarnings += hookOutput.summary.totalWarnings;
57144
+ }
56797
57145
  if (noFlagProvided || lintInstructionsFlag) {
56798
57146
  const rawOutput = await lintInstructions(targetDir);
56799
57147
  const instructionOutput = applySeverityFilter(rawOutput, rulesConfig);
@@ -56804,6 +57152,7 @@ function hasFrontmatterDelimiters(content) {
56804
57152
  const targetLabels = {
56805
57153
  skill: "skill(s)",
56806
57154
  agent: "agent(s)",
57155
+ hook: "hook config(s)",
56807
57156
  instruction: "instruction file(s)"
56808
57157
  };
56809
57158
  if (format !== "human") {