@lousy-agents/cli 5.6.2 → 5.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/copilot-with-fastify/.devcontainer/devcontainer.json +1 -1
- package/api/copilot-with-fastify/package-lock.json +225 -174
- package/api/copilot-with-fastify/package.json +3 -3
- package/cli/copilot-with-citty/.devcontainer/devcontainer.json +1 -1
- package/cli/copilot-with-citty/package.json +3 -3
- package/dist/index.js +353 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/copilot-with-react/.devcontainer/devcontainer.json +1 -1
- package/ui/copilot-with-react/package.json +4 -4
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@biomejs/biome": "2.4.9",
|
|
29
|
-
"@lousy-agents/mcp": "5.
|
|
29
|
+
"@lousy-agents/mcp": "5.7.0",
|
|
30
30
|
"@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
|
|
31
31
|
"@testcontainers/postgresql": "11.13.0",
|
|
32
32
|
"@types/node": "24.12.0",
|
|
33
|
-
"@upstash/context7-mcp": "2.1.
|
|
33
|
+
"@upstash/context7-mcp": "2.1.6",
|
|
34
34
|
"chance": "1.1.13",
|
|
35
35
|
"testcontainers": "11.13.0",
|
|
36
36
|
"tsx": "4.21.0",
|
|
37
37
|
"typescript": "6.0.2",
|
|
38
|
-
"vitest": "4.1.
|
|
38
|
+
"vitest": "4.1.2"
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"ghcr.io/devcontainers/features/copilot-cli:1.0.0": {},
|
|
12
12
|
"ghcr.io/anthropics/devcontainer-features/claude-code:1": {
|
|
13
|
-
"version": "v2.1.
|
|
13
|
+
"version": "v2.1.87"
|
|
14
14
|
},
|
|
15
15
|
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1.0.2": {
|
|
16
16
|
"packages": "yamllint, shellcheck",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@biomejs/biome": "2.4.9",
|
|
23
|
-
"@lousy-agents/mcp": "5.
|
|
23
|
+
"@lousy-agents/mcp": "5.7.0",
|
|
24
24
|
"@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
|
|
25
25
|
"@types/node": "24.12.0",
|
|
26
|
-
"@upstash/context7-mcp": "2.1.
|
|
26
|
+
"@upstash/context7-mcp": "2.1.6",
|
|
27
27
|
"chance": "1.1.13",
|
|
28
28
|
"tsx": "4.21.0",
|
|
29
29
|
"typescript": "6.0.2",
|
|
30
|
-
"vitest": "4.1.
|
|
30
|
+
"vitest": "4.1.2"
|
|
31
31
|
}
|
|
32
32
|
}
|
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 =
|
|
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
|
|
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") {
|