@lousy-agents/cli 5.18.2 → 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.
package/dist/index.js
CHANGED
|
@@ -3032,7 +3032,7 @@ exports.basename = (path, { windows } = {}) => {
|
|
|
3032
3032
|
|
|
3033
3033
|
|
|
3034
3034
|
},
|
|
3035
|
-
|
|
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
|
-
|
|
11692
|
+
$constructor("$ZodDiscriminatedUnion", (inst, def) => {
|
|
11693
11693
|
def.inclusive = false;
|
|
11694
11694
|
$ZodUnion.init(inst, def);
|
|
11695
11695
|
const _super = inst._zod.parse;
|
|
11696
|
-
|
|
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 =
|
|
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 (!
|
|
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__*/
|
|
15940
|
+
const ZodDiscriminatedUnion = /*@__PURE__*/ $constructor("ZodDiscriminatedUnion", (inst, def) => {
|
|
15941
15941
|
ZodUnion.init(inst, def);
|
|
15942
|
-
|
|
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
|
-
...
|
|
15950
|
+
...normalizeParams(params),
|
|
15951
15951
|
});
|
|
15952
15952
|
}
|
|
15953
15953
|
const ZodIntersection = /*@__PURE__*/ $constructor("ZodIntersection", (inst, def) => {
|
|
@@ -27697,13 +27697,13 @@ const PackageJsonSchema = schemas_object({
|
|
|
27697
27697
|
"shared"
|
|
27698
27698
|
]
|
|
27699
27699
|
},
|
|
27700
|
-
//
|
|
27700
|
+
// Additional skill roots (SKILL.md convention, no alternate layout documented)
|
|
27701
27701
|
{
|
|
27702
27702
|
id: "skill-pi",
|
|
27703
27703
|
path: ".pi/skills",
|
|
27704
27704
|
matchKind: "directory-prefix",
|
|
27705
27705
|
primaryConstruct: "skill",
|
|
27706
|
-
lintTarget: "
|
|
27706
|
+
lintTarget: "skills",
|
|
27707
27707
|
harnessHints: [
|
|
27708
27708
|
"pi"
|
|
27709
27709
|
]
|
|
@@ -27713,7 +27713,7 @@ const PackageJsonSchema = schemas_object({
|
|
|
27713
27713
|
path: ".pi/prompts",
|
|
27714
27714
|
matchKind: "directory-prefix",
|
|
27715
27715
|
primaryConstruct: "skill",
|
|
27716
|
-
lintTarget: "
|
|
27716
|
+
lintTarget: "skills",
|
|
27717
27717
|
harnessHints: [
|
|
27718
27718
|
"pi"
|
|
27719
27719
|
]
|
|
@@ -27833,13 +27833,13 @@ const PackageJsonSchema = schemas_object({
|
|
|
27833
27833
|
],
|
|
27834
27834
|
instructionFormat: "claude-md"
|
|
27835
27835
|
},
|
|
27836
|
-
//
|
|
27836
|
+
// Subagent lint root
|
|
27837
27837
|
{
|
|
27838
27838
|
id: "subagent-claude",
|
|
27839
27839
|
path: ".claude/agents",
|
|
27840
27840
|
matchKind: "directory-prefix",
|
|
27841
27841
|
primaryConstruct: "subagent",
|
|
27842
|
-
lintTarget: "
|
|
27842
|
+
lintTarget: "subagents",
|
|
27843
27843
|
harnessHints: [
|
|
27844
27844
|
"claude"
|
|
27845
27845
|
]
|
|
@@ -27875,11 +27875,14 @@ const PackageJsonSchema = schemas_object({
|
|
|
27875
27875
|
* Normalizes a repo-relative path for catalog matching.
|
|
27876
27876
|
* Collapses `.` / `..` segments. Returns "" when the path is absolute or
|
|
27877
27877
|
* escapes the repository root — callers treat "" as non-matching.
|
|
27878
|
-
*/ function
|
|
27879
|
-
const normalized = path.replaceAll("\\", "/");
|
|
27878
|
+
*/ function isAbsoluteRepoPath(normalized) {
|
|
27880
27879
|
// POSIX absolute, UNC-style leading slash after backslash conversion,
|
|
27881
27880
|
// and Windows drive-letter absolute (C:/..., d:\...).
|
|
27882
|
-
|
|
27881
|
+
return normalized.startsWith("/") || /^[A-Za-z]:(\/|$)/.test(normalized);
|
|
27882
|
+
}
|
|
27883
|
+
function normalizeRepoRelativePath(path) {
|
|
27884
|
+
const normalized = path.replaceAll("\\", "/");
|
|
27885
|
+
if (isAbsoluteRepoPath(normalized)) {
|
|
27883
27886
|
return "";
|
|
27884
27887
|
}
|
|
27885
27888
|
const resolved = [];
|
|
@@ -27887,10 +27890,10 @@ const PackageJsonSchema = schemas_object({
|
|
|
27887
27890
|
if (segment === "" || segment === ".") {
|
|
27888
27891
|
continue;
|
|
27889
27892
|
}
|
|
27893
|
+
if (segment === ".." && resolved.length === 0) {
|
|
27894
|
+
return "";
|
|
27895
|
+
}
|
|
27890
27896
|
if (segment === "..") {
|
|
27891
|
-
if (resolved.length === 0) {
|
|
27892
|
-
return "";
|
|
27893
|
-
}
|
|
27894
27897
|
resolved.pop();
|
|
27895
27898
|
continue;
|
|
27896
27899
|
}
|
|
@@ -27938,14 +27941,15 @@ function constructTypesForPath(path, catalog = AGENTIC_LOCATION_CATALOG) {
|
|
|
27938
27941
|
if (!best) {
|
|
27939
27942
|
return [];
|
|
27940
27943
|
}
|
|
27941
|
-
|
|
27944
|
+
const secondary = best.secondaryConstructs ?? [];
|
|
27945
|
+
if (secondary.length === 0) {
|
|
27942
27946
|
return [
|
|
27943
|
-
best.primaryConstruct
|
|
27944
|
-
...best.secondaryConstructs
|
|
27947
|
+
best.primaryConstruct
|
|
27945
27948
|
];
|
|
27946
27949
|
}
|
|
27947
27950
|
return [
|
|
27948
|
-
best.primaryConstruct
|
|
27951
|
+
best.primaryConstruct,
|
|
27952
|
+
...secondary
|
|
27949
27953
|
];
|
|
27950
27954
|
}
|
|
27951
27955
|
function lintTargetEntries(target, catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
@@ -27957,8 +27961,14 @@ function skillDirectoryRoots(catalog = agentic_location_catalog_AGENTIC_LOCATION
|
|
|
27957
27961
|
function agentDirectoryRoots(catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
27958
27962
|
return lintTargetEntries("agents", catalog).filter((entry)=>entry.matchKind === "directory-prefix").map((entry)=>entry.path);
|
|
27959
27963
|
}
|
|
27964
|
+
function subagentDirectoryRoots(catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
27965
|
+
return lintTargetEntries("subagents", catalog).filter((entry)=>entry.matchKind === "directory-prefix").map((entry)=>entry.path);
|
|
27966
|
+
}
|
|
27967
|
+
function isHookConfigEntry(entry) {
|
|
27968
|
+
return entry.matchKind === "exact" && entry.hookPlatform != null;
|
|
27969
|
+
}
|
|
27960
27970
|
function hookConfigPaths(catalog = agentic_location_catalog_AGENTIC_LOCATION_CATALOG) {
|
|
27961
|
-
return lintTargetEntries("hooks", catalog).filter(
|
|
27971
|
+
return lintTargetEntries("hooks", catalog).filter(isHookConfigEntry).map((entry)=>({
|
|
27962
27972
|
relativePath: entry.path,
|
|
27963
27973
|
platform: entry.hookPlatform
|
|
27964
27974
|
}));
|
|
@@ -30372,26 +30382,74 @@ function parseRawEdges(content) {
|
|
|
30372
30382
|
return edges;
|
|
30373
30383
|
}
|
|
30374
30384
|
|
|
30375
|
-
;// CONCATENATED MODULE: ../
|
|
30376
|
-
|
|
30377
|
-
|
|
30378
|
-
|
|
30379
|
-
|
|
30385
|
+
;// CONCATENATED MODULE: ../core/src/entities/mcp-config-source.ts
|
|
30386
|
+
/**
|
|
30387
|
+
* Canonical list of MCP server configuration file locations.
|
|
30388
|
+
* Shared by doctor (inventory enumeration) and lint (mcp-servers target) so
|
|
30389
|
+
* the two consumers cannot drift on where MCP servers are declared.
|
|
30390
|
+
*
|
|
30391
|
+
* Deliberately kept separate from AGENTIC_LOCATION_CATALOG: one MCP config
|
|
30392
|
+
* file can declare many servers, so the one-path-to-one-construct model the
|
|
30393
|
+
* catalog/matchers use for skills, agents, hooks, and instructions does not
|
|
30394
|
+
* fit. This list is the single source of truth for MCP config paths instead.
|
|
30395
|
+
*/ const MCP_CONFIG_SOURCES = [
|
|
30380
30396
|
{
|
|
30381
30397
|
relPath: ".mcp.json",
|
|
30382
|
-
|
|
30398
|
+
harnessHint: "shared"
|
|
30383
30399
|
},
|
|
30384
30400
|
{
|
|
30385
30401
|
relPath: ".vscode/mcp.json",
|
|
30386
|
-
|
|
30402
|
+
harnessHint: "copilot"
|
|
30387
30403
|
}
|
|
30388
30404
|
];
|
|
30405
|
+
|
|
30406
|
+
;// CONCATENATED MODULE: ../doctor/src/gateways/mcp-config.ts
|
|
30407
|
+
|
|
30408
|
+
|
|
30409
|
+
|
|
30410
|
+
const MAX_MCP_CONFIG_BYTES = 1_048_576;
|
|
30389
30411
|
const McpServerEntrySchema = schemas_object({
|
|
30390
30412
|
type: schemas_string().optional(),
|
|
30391
30413
|
transport: schemas_string().optional()
|
|
30392
30414
|
}).passthrough();
|
|
30415
|
+
/**
|
|
30416
|
+
* Property names that must never be used as MCP server names: assigning
|
|
30417
|
+
* them via a plain object key (as zod's `record()` does when reconstructing
|
|
30418
|
+
* parsed output) triggers JavaScript's `__proto__` magic setter instead of
|
|
30419
|
+
* creating an own property, silently dropping the entry. Rejected explicitly
|
|
30420
|
+
* so a config declaring one of these names is treated as invalid instead of
|
|
30421
|
+
* silently under-inventoried.
|
|
30422
|
+
*/ const RESERVED_SERVER_NAMES = [
|
|
30423
|
+
"__proto__",
|
|
30424
|
+
"constructor",
|
|
30425
|
+
"prototype"
|
|
30426
|
+
];
|
|
30427
|
+
const SERVER_MAP_FIELDS = [
|
|
30428
|
+
"mcpServers",
|
|
30429
|
+
"servers"
|
|
30430
|
+
];
|
|
30431
|
+
function hasReservedServerName(parsed) {
|
|
30432
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
30433
|
+
return false;
|
|
30434
|
+
}
|
|
30435
|
+
for (const field of SERVER_MAP_FIELDS){
|
|
30436
|
+
const servers = parsed[field];
|
|
30437
|
+
if (typeof servers !== "object" || servers === null) {
|
|
30438
|
+
continue;
|
|
30439
|
+
}
|
|
30440
|
+
if (RESERVED_SERVER_NAMES.some((name)=>Object.hasOwn(servers, name))) {
|
|
30441
|
+
return true;
|
|
30442
|
+
}
|
|
30443
|
+
}
|
|
30444
|
+
return false;
|
|
30445
|
+
}
|
|
30446
|
+
// Accepts both Claude Code's `.mcp.json` convention (`mcpServers`) and VS
|
|
30447
|
+
// Code's `.vscode/mcp.json` convention (`servers`) regardless of which
|
|
30448
|
+
// source file is being read, since either config file could in practice
|
|
30449
|
+
// use either key.
|
|
30393
30450
|
const McpConfigSchema = schemas_object({
|
|
30394
|
-
mcpServers: schemas_record(schemas_string(), McpServerEntrySchema).optional()
|
|
30451
|
+
mcpServers: schemas_record(schemas_string(), McpServerEntrySchema).optional(),
|
|
30452
|
+
servers: schemas_record(schemas_string(), McpServerEntrySchema).optional()
|
|
30395
30453
|
}).passthrough();
|
|
30396
30454
|
async function readMcpServersFromSource(repoRoot, source) {
|
|
30397
30455
|
let content;
|
|
@@ -30406,13 +30464,20 @@ async function readMcpServersFromSource(repoRoot, source) {
|
|
|
30406
30464
|
} catch {
|
|
30407
30465
|
return [];
|
|
30408
30466
|
}
|
|
30467
|
+
if (hasReservedServerName(raw)) {
|
|
30468
|
+
return [];
|
|
30469
|
+
}
|
|
30409
30470
|
const result = McpConfigSchema.safeParse(raw);
|
|
30410
|
-
if (!result.success
|
|
30471
|
+
if (!result.success) {
|
|
30411
30472
|
return [];
|
|
30412
30473
|
}
|
|
30413
|
-
|
|
30474
|
+
const servers = {
|
|
30475
|
+
...result.data.mcpServers ?? {},
|
|
30476
|
+
...result.data.servers ?? {}
|
|
30477
|
+
};
|
|
30478
|
+
return Object.entries(servers).map(([serverName, entry])=>({
|
|
30414
30479
|
serverName,
|
|
30415
|
-
harness: source.
|
|
30480
|
+
harness: source.harnessHint,
|
|
30416
30481
|
path: source.relPath,
|
|
30417
30482
|
...entry.transport !== undefined ? {
|
|
30418
30483
|
transport: entry.transport
|
|
@@ -32855,6 +32920,82 @@ function createInitHooksCommand(useCase) {
|
|
|
32855
32920
|
}
|
|
32856
32921
|
}
|
|
32857
32922
|
|
|
32923
|
+
;// CONCATENATED MODULE: ../core/src/gateways/markdown-file-discovery.ts
|
|
32924
|
+
/**
|
|
32925
|
+
* Shared recursive markdown-file discovery for lint gateways that scan a
|
|
32926
|
+
* catalog root directory for `.md` files (agents, subagents). Guards
|
|
32927
|
+
* against path traversal and symlink escapes, and caps recursion depth to
|
|
32928
|
+
* avoid stack exhaustion on pathologically deep directory trees.
|
|
32929
|
+
*/
|
|
32930
|
+
|
|
32931
|
+
/** Maximum subdirectory nesting the walk will descend into. */ const MAX_WALK_DEPTH = 32;
|
|
32932
|
+
function isUnsafeEntryName(name) {
|
|
32933
|
+
return name.includes("..") || name.includes("/") || name.includes("\\");
|
|
32934
|
+
}
|
|
32935
|
+
function isWithinResolvedRoot(resolvedRootDir, absoluteEntryPath) {
|
|
32936
|
+
const resolvedPath = (0,external_node_path_.resolve)(absoluteEntryPath);
|
|
32937
|
+
const rel = (0,external_node_path_.relative)(resolvedRootDir, resolvedPath);
|
|
32938
|
+
return !rel.startsWith("..") && !rel.startsWith(external_node_path_.sep);
|
|
32939
|
+
}
|
|
32940
|
+
async function rootExists(targetDir, rootDir) {
|
|
32941
|
+
try {
|
|
32942
|
+
return await file_system_utils_pathExistsWithinRoot(targetDir, rootDir);
|
|
32943
|
+
} catch {
|
|
32944
|
+
return false;
|
|
32945
|
+
}
|
|
32946
|
+
}
|
|
32947
|
+
function isDiscoverableMarkdownFile(entry, name) {
|
|
32948
|
+
return entry.isFile() && name.endsWith(".md");
|
|
32949
|
+
}
|
|
32950
|
+
async function collectEntryFiles(targetDir, resolvedRootDir, dir, depth, entry, deriveName) {
|
|
32951
|
+
const name = entry.name;
|
|
32952
|
+
if (isUnsafeEntryName(name)) {
|
|
32953
|
+
return [];
|
|
32954
|
+
}
|
|
32955
|
+
const entryPath = (0,external_node_path_.join)(dir, name);
|
|
32956
|
+
const absoluteEntryPath = (0,external_node_path_.join)(targetDir, entryPath);
|
|
32957
|
+
if (!isWithinResolvedRoot(resolvedRootDir, absoluteEntryPath)) {
|
|
32958
|
+
return [];
|
|
32959
|
+
}
|
|
32960
|
+
if (entry.isSymbolicLink()) {
|
|
32961
|
+
return [];
|
|
32962
|
+
}
|
|
32963
|
+
if (entry.isDirectory()) {
|
|
32964
|
+
return walkMarkdownFiles(targetDir, resolvedRootDir, entryPath, depth + 1, deriveName);
|
|
32965
|
+
}
|
|
32966
|
+
if (!isDiscoverableMarkdownFile(entry, name)) {
|
|
32967
|
+
return [];
|
|
32968
|
+
}
|
|
32969
|
+
return [
|
|
32970
|
+
{
|
|
32971
|
+
filePath: absoluteEntryPath,
|
|
32972
|
+
name: deriveName(name)
|
|
32973
|
+
}
|
|
32974
|
+
];
|
|
32975
|
+
}
|
|
32976
|
+
async function walkMarkdownFiles(targetDir, resolvedRootDir, dir, depth, deriveName) {
|
|
32977
|
+
if (depth > MAX_WALK_DEPTH) {
|
|
32978
|
+
return [];
|
|
32979
|
+
}
|
|
32980
|
+
const entries = await file_system_utils_listDirectoryWithinRoot(targetDir, dir);
|
|
32981
|
+
const discovered = [];
|
|
32982
|
+
for (const entry of entries){
|
|
32983
|
+
const files = await collectEntryFiles(targetDir, resolvedRootDir, dir, depth, entry, deriveName);
|
|
32984
|
+
discovered.push(...files);
|
|
32985
|
+
}
|
|
32986
|
+
return discovered;
|
|
32987
|
+
}
|
|
32988
|
+
/**
|
|
32989
|
+
* Recursively discovers `.md` files under `rootDir` (relative to
|
|
32990
|
+
* `targetDir`). `deriveName` maps a filename to the construct name callers
|
|
32991
|
+
* use to identify the file (e.g. stripping `.md` or `.agent.md`).
|
|
32992
|
+
*/ async function discoverMarkdownFiles(targetDir, rootDir, deriveName) {
|
|
32993
|
+
if (!await rootExists(targetDir, rootDir)) {
|
|
32994
|
+
return [];
|
|
32995
|
+
}
|
|
32996
|
+
return walkMarkdownFiles(targetDir, (0,external_node_path_.resolve)(targetDir, rootDir), rootDir, 0, deriveName);
|
|
32997
|
+
}
|
|
32998
|
+
|
|
32858
32999
|
;// CONCATENATED MODULE: ../core/src/gateways/agent-lint-gateway.ts
|
|
32859
33000
|
/**
|
|
32860
33001
|
* Gateway for agent lint file system operations.
|
|
@@ -32863,6 +33004,7 @@ function createInitHooksCommand(useCase) {
|
|
|
32863
33004
|
|
|
32864
33005
|
|
|
32865
33006
|
|
|
33007
|
+
|
|
32866
33008
|
/** Maximum agent file size: 1 MB */ const MAX_AGENT_FILE_BYTES = 1_048_576;
|
|
32867
33009
|
/** Catalog agent roots converted to OS-native relative paths. */ const AGENT_DIRECTORIES = agentDirectoryRoots().map((root)=>(0,external_node_path_.join)(...root.split("/")));
|
|
32868
33010
|
/**
|
|
@@ -32877,51 +33019,11 @@ function createInitHooksCommand(useCase) {
|
|
|
32877
33019
|
return agents;
|
|
32878
33020
|
}
|
|
32879
33021
|
async discoverAgentsInDir(targetDir, agentsDir) {
|
|
32880
|
-
|
|
32881
|
-
|
|
32882
|
-
|
|
32883
|
-
|
|
32884
|
-
|
|
32885
|
-
return [];
|
|
32886
|
-
}
|
|
32887
|
-
const resolvedAgentsDir = (0,external_node_path_.resolve)(targetDir, agentsDir);
|
|
32888
|
-
const agents = [];
|
|
32889
|
-
const walk = async (dir)=>{
|
|
32890
|
-
const entries = await file_system_utils_listDirectoryWithinRoot(targetDir, dir);
|
|
32891
|
-
for (const entry of entries){
|
|
32892
|
-
const name = entry.name;
|
|
32893
|
-
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
32894
|
-
continue;
|
|
32895
|
-
}
|
|
32896
|
-
const entryPath = (0,external_node_path_.join)(dir, name);
|
|
32897
|
-
const absoluteEntryPath = (0,external_node_path_.join)(targetDir, entryPath);
|
|
32898
|
-
const resolvedPath = (0,external_node_path_.resolve)(absoluteEntryPath);
|
|
32899
|
-
const rel = (0,external_node_path_.relative)(resolvedAgentsDir, resolvedPath);
|
|
32900
|
-
if (rel.startsWith("..") || rel.startsWith(external_node_path_.sep)) {
|
|
32901
|
-
continue;
|
|
32902
|
-
}
|
|
32903
|
-
if (entry.isSymbolicLink()) {
|
|
32904
|
-
continue;
|
|
32905
|
-
}
|
|
32906
|
-
if (entry.isDirectory()) {
|
|
32907
|
-
await walk(entryPath);
|
|
32908
|
-
continue;
|
|
32909
|
-
}
|
|
32910
|
-
if (!entry.isFile()) {
|
|
32911
|
-
continue;
|
|
32912
|
-
}
|
|
32913
|
-
if (!name.endsWith(".md")) {
|
|
32914
|
-
continue;
|
|
32915
|
-
}
|
|
32916
|
-
const agentName = name.endsWith(".agent.md") ? (0,external_node_path_.basename)(name, ".agent.md") : (0,external_node_path_.basename)(name, ".md");
|
|
32917
|
-
agents.push({
|
|
32918
|
-
filePath: absoluteEntryPath,
|
|
32919
|
-
agentName
|
|
32920
|
-
});
|
|
32921
|
-
}
|
|
32922
|
-
};
|
|
32923
|
-
await walk(agentsDir);
|
|
32924
|
-
return agents;
|
|
33022
|
+
const files = await discoverMarkdownFiles(targetDir, agentsDir, (name)=>name.endsWith(".agent.md") ? (0,external_node_path_.basename)(name, ".agent.md") : (0,external_node_path_.basename)(name, ".md"));
|
|
33023
|
+
return files.map((file)=>({
|
|
33024
|
+
filePath: file.filePath,
|
|
33025
|
+
agentName: file.name
|
|
33026
|
+
}));
|
|
32925
33027
|
}
|
|
32926
33028
|
async readAgentFileContent(filePath) {
|
|
32927
33029
|
return readFileNoFollow(filePath, MAX_AGENT_FILE_BYTES);
|
|
@@ -32945,11 +33047,28 @@ function createInitHooksCommand(useCase) {
|
|
|
32945
33047
|
|
|
32946
33048
|
/** Maximum hook config file size: 1 MB */ const MAX_CONFIG_FILE_BYTES = 1_048_576;
|
|
32947
33049
|
/** Matches the Copilot hook key `"preToolUse":` to detect hook section presence */ const COPILOT_HOOK_PATTERN = /"preToolUse"\s*:/;
|
|
32948
|
-
/** Matches the Claude hook key `"
|
|
33050
|
+
/** Matches the Claude hook key `"hooks":` to detect hook section presence */ const CLAUDE_HOOK_PATTERN = /"hooks"\s*:/;
|
|
32949
33051
|
/** Catalog hook config paths converted to OS-native relative paths. */ const HOOK_CONFIG_PATHS = hookConfigPaths().map((entry)=>({
|
|
32950
33052
|
relativePath: (0,external_node_path_.join)(...entry.relativePath.split("/")),
|
|
32951
33053
|
platform: entry.platform
|
|
32952
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
|
+
}
|
|
32953
33072
|
/**
|
|
32954
33073
|
* File system implementation of the hook config lint gateway.
|
|
32955
33074
|
*/ class FileSystemHookConfigGateway {
|
|
@@ -32996,14 +33115,24 @@ function createInitHooksCommand(useCase) {
|
|
|
32996
33115
|
return readFileNoFollow(filePath, MAX_CONFIG_FILE_BYTES);
|
|
32997
33116
|
}
|
|
32998
33117
|
/**
|
|
32999
|
-
*
|
|
33000
|
-
*
|
|
33001
|
-
*
|
|
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.
|
|
33002
33125
|
*/ mayContainHookSection(content, platform) {
|
|
33003
33126
|
if (platform === "copilot") {
|
|
33004
33127
|
return COPILOT_HOOK_PATTERN.test(content);
|
|
33005
33128
|
}
|
|
33006
|
-
|
|
33129
|
+
let parsed;
|
|
33130
|
+
try {
|
|
33131
|
+
parsed = JSON.parse(content);
|
|
33132
|
+
} catch {
|
|
33133
|
+
return CLAUDE_HOOK_PATTERN.test(content);
|
|
33134
|
+
}
|
|
33135
|
+
return hasHooksSection(parsed);
|
|
33007
33136
|
}
|
|
33008
33137
|
}
|
|
33009
33138
|
/**
|
|
@@ -52594,6 +52723,120 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
52594
52723
|
return new RemarkMarkdownAstGateway();
|
|
52595
52724
|
}
|
|
52596
52725
|
|
|
52726
|
+
;// CONCATENATED MODULE: ../core/src/gateways/mcp-lint-gateway.ts
|
|
52727
|
+
/**
|
|
52728
|
+
* Gateway for MCP server config file system operations.
|
|
52729
|
+
* Discovers MCP server config files at the shared MCP_CONFIG_SOURCES paths.
|
|
52730
|
+
*/
|
|
52731
|
+
|
|
52732
|
+
|
|
52733
|
+
/** Maximum MCP config file size: 1 MB */ const MAX_MCP_CONFIG_FILE_BYTES = 1_048_576;
|
|
52734
|
+
/** Catalog MCP config paths converted to OS-native relative paths. */ const MCP_CONFIG_RELATIVE_PATHS = MCP_CONFIG_SOURCES.map((source)=>(0,external_node_path_.join)(...source.relPath.split("/")));
|
|
52735
|
+
const SKIPPABLE_PATH_ERROR_PREFIXES = [
|
|
52736
|
+
"Resolved path is outside target directory:",
|
|
52737
|
+
"Path contains symbolic link:"
|
|
52738
|
+
];
|
|
52739
|
+
function isSkippablePathError(error) {
|
|
52740
|
+
if (!(error instanceof Error)) {
|
|
52741
|
+
return false;
|
|
52742
|
+
}
|
|
52743
|
+
return SKIPPABLE_PATH_ERROR_PREFIXES.some((prefix)=>error.message.startsWith(prefix));
|
|
52744
|
+
}
|
|
52745
|
+
async function pathExistsSafely(targetDir, relativePath) {
|
|
52746
|
+
try {
|
|
52747
|
+
return await file_system_utils_pathExistsWithinRoot(targetDir, relativePath);
|
|
52748
|
+
} catch {
|
|
52749
|
+
return false;
|
|
52750
|
+
}
|
|
52751
|
+
}
|
|
52752
|
+
async function resolveConfigPathOrNull(targetDir, relativePath) {
|
|
52753
|
+
try {
|
|
52754
|
+
return await file_system_utils_resolveSafePath(targetDir, relativePath);
|
|
52755
|
+
} catch (error) {
|
|
52756
|
+
if (isSkippablePathError(error)) {
|
|
52757
|
+
return null;
|
|
52758
|
+
}
|
|
52759
|
+
throw error;
|
|
52760
|
+
}
|
|
52761
|
+
}
|
|
52762
|
+
/**
|
|
52763
|
+
* Resolves a config path within the target directory, returning null when the
|
|
52764
|
+
* path is missing or escapes the root via traversal/symlink.
|
|
52765
|
+
*/ async function tryResolveConfigPath(targetDir, relativePath) {
|
|
52766
|
+
if (!await pathExistsSafely(targetDir, relativePath)) {
|
|
52767
|
+
return null;
|
|
52768
|
+
}
|
|
52769
|
+
return resolveConfigPathOrNull(targetDir, relativePath);
|
|
52770
|
+
}
|
|
52771
|
+
/**
|
|
52772
|
+
* File system implementation of the MCP server lint gateway.
|
|
52773
|
+
*/ class FileSystemMcpServersLintGateway {
|
|
52774
|
+
async discoverMcpConfigFiles(targetDir) {
|
|
52775
|
+
const discovered = [];
|
|
52776
|
+
for (const relativePath of MCP_CONFIG_RELATIVE_PATHS){
|
|
52777
|
+
const safePath = await tryResolveConfigPath(targetDir, relativePath);
|
|
52778
|
+
if (safePath === null) {
|
|
52779
|
+
continue;
|
|
52780
|
+
}
|
|
52781
|
+
discovered.push({
|
|
52782
|
+
filePath: safePath,
|
|
52783
|
+
relativePath
|
|
52784
|
+
});
|
|
52785
|
+
}
|
|
52786
|
+
return discovered;
|
|
52787
|
+
}
|
|
52788
|
+
async readFileContent(filePath) {
|
|
52789
|
+
return readFileNoFollow(filePath, MAX_MCP_CONFIG_FILE_BYTES);
|
|
52790
|
+
}
|
|
52791
|
+
}
|
|
52792
|
+
/**
|
|
52793
|
+
* Creates and returns the default MCP server lint gateway.
|
|
52794
|
+
*/ function createMcpServersLintGateway() {
|
|
52795
|
+
return new FileSystemMcpServersLintGateway();
|
|
52796
|
+
}
|
|
52797
|
+
|
|
52798
|
+
;// CONCATENATED MODULE: ../core/src/gateways/subagent-lint-gateway.ts
|
|
52799
|
+
/**
|
|
52800
|
+
* Gateway for subagent lint file system operations.
|
|
52801
|
+
* Discovers Claude Code subagent files and parses YAML frontmatter.
|
|
52802
|
+
*/
|
|
52803
|
+
|
|
52804
|
+
|
|
52805
|
+
|
|
52806
|
+
|
|
52807
|
+
/** Maximum subagent file size: 1 MB */ const MAX_SUBAGENT_FILE_BYTES = 1_048_576;
|
|
52808
|
+
/** Catalog subagent roots converted to OS-native relative paths. */ const SUBAGENT_DIRECTORIES = subagentDirectoryRoots().map((root)=>(0,external_node_path_.join)(...root.split("/")));
|
|
52809
|
+
/**
|
|
52810
|
+
* File system implementation of the subagent lint gateway.
|
|
52811
|
+
*/ class FileSystemSubagentLintGateway {
|
|
52812
|
+
async discoverSubagents(targetDir) {
|
|
52813
|
+
const subagents = [];
|
|
52814
|
+
for (const subagentsDir of SUBAGENT_DIRECTORIES){
|
|
52815
|
+
const discovered = await this.discoverSubagentsInDir(targetDir, subagentsDir);
|
|
52816
|
+
subagents.push(...discovered);
|
|
52817
|
+
}
|
|
52818
|
+
return subagents;
|
|
52819
|
+
}
|
|
52820
|
+
async discoverSubagentsInDir(targetDir, subagentsDir) {
|
|
52821
|
+
const files = await discoverMarkdownFiles(targetDir, subagentsDir, (name)=>(0,external_node_path_.basename)(name, ".md"));
|
|
52822
|
+
return files.map((file)=>({
|
|
52823
|
+
filePath: file.filePath,
|
|
52824
|
+
subagentName: file.name
|
|
52825
|
+
}));
|
|
52826
|
+
}
|
|
52827
|
+
async readSubagentFileContent(filePath) {
|
|
52828
|
+
return readFileNoFollow(filePath, MAX_SUBAGENT_FILE_BYTES);
|
|
52829
|
+
}
|
|
52830
|
+
parseFrontmatter(content) {
|
|
52831
|
+
return parseFrontmatter(content);
|
|
52832
|
+
}
|
|
52833
|
+
}
|
|
52834
|
+
/**
|
|
52835
|
+
* Creates and returns the default subagent lint gateway.
|
|
52836
|
+
*/ function createSubagentLintGateway() {
|
|
52837
|
+
return new FileSystemSubagentLintGateway();
|
|
52838
|
+
}
|
|
52839
|
+
|
|
52597
52840
|
;// CONCATENATED MODULE: ../core/src/entities/lint-rules.ts
|
|
52598
52841
|
/**
|
|
52599
52842
|
* Lint rule registry entity.
|
|
@@ -52609,10 +52852,31 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
52609
52852
|
"agent/invalid-description": "error",
|
|
52610
52853
|
"agent/invalid-field": "warn"
|
|
52611
52854
|
},
|
|
52855
|
+
subagents: {
|
|
52856
|
+
"subagent/missing-frontmatter": "error",
|
|
52857
|
+
"subagent/invalid-frontmatter": "error",
|
|
52858
|
+
"subagent/missing-name": "error",
|
|
52859
|
+
// Claude Code's documented subagent contract requires only `name`
|
|
52860
|
+
// and `description`; it neither specifies a name format beyond
|
|
52861
|
+
// "unique identifier" nor ties `name` to the filename the way
|
|
52862
|
+
// VS Code derives a Copilot custom agent's name from
|
|
52863
|
+
// `<name>.agent.md`. Defaulting these to "warn" avoids failing
|
|
52864
|
+
// lint for subagents that are valid per Claude Code's own docs.
|
|
52865
|
+
"subagent/invalid-name-format": "warn",
|
|
52866
|
+
"subagent/name-mismatch": "warn",
|
|
52867
|
+
"subagent/missing-description": "error",
|
|
52868
|
+
"subagent/invalid-description": "error",
|
|
52869
|
+
"subagent/invalid-field": "warn"
|
|
52870
|
+
},
|
|
52871
|
+
mcpServers: {
|
|
52872
|
+
"mcpserver/invalid-json": "error",
|
|
52873
|
+
"mcpserver/invalid-config": "error"
|
|
52874
|
+
},
|
|
52612
52875
|
hooks: {
|
|
52613
52876
|
"hook/invalid-json": "error",
|
|
52614
52877
|
"hook/invalid-config": "error",
|
|
52615
52878
|
"hook/missing-command": "error",
|
|
52879
|
+
"hook/unknown-event": "error",
|
|
52616
52880
|
"hook/missing-matcher": "warn",
|
|
52617
52881
|
"hook/missing-timeout": "warn"
|
|
52618
52882
|
},
|
|
@@ -52650,9 +52914,11 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
52650
52914
|
]));
|
|
52651
52915
|
/** Zod schema for the lint.rules section of the config */ const LintRulesConfigSchema = schemas_object({
|
|
52652
52916
|
agents: RuleConfigMapSchema.optional(),
|
|
52917
|
+
subagents: RuleConfigMapSchema.optional(),
|
|
52653
52918
|
hooks: RuleConfigMapSchema.optional(),
|
|
52654
52919
|
instructions: RuleConfigMapSchema.optional(),
|
|
52655
|
-
skills: RuleConfigMapSchema.optional()
|
|
52920
|
+
skills: RuleConfigMapSchema.optional(),
|
|
52921
|
+
mcpServers: RuleConfigMapSchema.optional()
|
|
52656
52922
|
});
|
|
52657
52923
|
/** Zod schema for the lint section of the config */ const LintConfigSchema = schemas_object({
|
|
52658
52924
|
lint: schemas_object({
|
|
@@ -52695,9 +52961,11 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
52695
52961
|
}
|
|
52696
52962
|
return {
|
|
52697
52963
|
agents: mergeTargetRules(DEFAULT_LINT_RULES.agents, rules.agents),
|
|
52964
|
+
subagents: mergeTargetRules(DEFAULT_LINT_RULES.subagents, rules.subagents),
|
|
52698
52965
|
hooks: mergeTargetRules(DEFAULT_LINT_RULES.hooks, rules.hooks),
|
|
52699
52966
|
instructions: mergeTargetRules(DEFAULT_LINT_RULES.instructions, rules.instructions),
|
|
52700
|
-
skills: mergeTargetRules(DEFAULT_LINT_RULES.skills, rules.skills)
|
|
52967
|
+
skills: mergeTargetRules(DEFAULT_LINT_RULES.skills, rules.skills),
|
|
52968
|
+
mcpServers: mergeTargetRules(DEFAULT_LINT_RULES.mcpServers, rules.mcpServers)
|
|
52701
52969
|
};
|
|
52702
52970
|
}
|
|
52703
52971
|
|
|
@@ -52709,8 +52977,10 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
52709
52977
|
*/ /** Maps a lint target to its config key */ const TARGET_TO_CONFIG_KEY = {
|
|
52710
52978
|
skill: "skills",
|
|
52711
52979
|
agent: "agents",
|
|
52980
|
+
subagent: "subagents",
|
|
52712
52981
|
hook: "hooks",
|
|
52713
|
-
instruction: "instructions"
|
|
52982
|
+
instruction: "instructions",
|
|
52983
|
+
"mcp-server": "mcpServers"
|
|
52714
52984
|
};
|
|
52715
52985
|
/**
|
|
52716
52986
|
* Maps config severity to diagnostic severity.
|
|
@@ -52911,6 +53181,167 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
52911
53181
|
return lines[0]?.trim() === "---";
|
|
52912
53182
|
}
|
|
52913
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
|
+
|
|
52914
53345
|
;// CONCATENATED MODULE: ../core/src/entities/copilot-hook-schema.ts
|
|
52915
53346
|
/**
|
|
52916
53347
|
* Zod schemas for the GitHub Copilot hooks configuration format.
|
|
@@ -52922,7 +53353,7 @@ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESC
|
|
|
52922
53353
|
* depend on @lousy-agents/core.
|
|
52923
53354
|
*/
|
|
52924
53355
|
const MAX_HOOKS_PER_EVENT = 100;
|
|
52925
|
-
/** Regex that allows standard env var names and rejects __proto__ (the prototype-polluting key). */ const
|
|
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_]*$/;
|
|
52926
53357
|
/**
|
|
52927
53358
|
* Zod schema for a single GitHub Copilot hook command entry.
|
|
52928
53359
|
*/ const CopilotHookCommandSchema = schemas_object({
|
|
@@ -52931,7 +53362,7 @@ const MAX_HOOKS_PER_EVENT = 100;
|
|
|
52931
53362
|
powershell: schemas_string().min(1, "Hook PowerShell command must not be empty").optional(),
|
|
52932
53363
|
cwd: schemas_string().optional(),
|
|
52933
53364
|
timeoutSec: schemas_number().positive().optional(),
|
|
52934
|
-
env: schemas_record(schemas_string().regex(
|
|
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()
|
|
52935
53366
|
}).strict().refine((data)=>Boolean(data.bash) || Boolean(data.powershell), {
|
|
52936
53367
|
message: "At least one of 'bash' or 'powershell' must be provided and non-empty"
|
|
52937
53368
|
});
|
|
@@ -52951,33 +53382,78 @@ const hookArray = schemas_array(CopilotHookCommandSchema).max(MAX_HOOKS_PER_EVEN
|
|
|
52951
53382
|
}).strict();
|
|
52952
53383
|
|
|
52953
53384
|
;// CONCATENATED MODULE: ../core/src/use-cases/lint-hook-config.ts
|
|
52954
|
-
// biome-ignore-all lint/style/useNamingConvention: Claude Code API uses PascalCase hook event names (PreToolUse)
|
|
52955
53385
|
/**
|
|
52956
|
-
* Use case for linting
|
|
53386
|
+
* Use case for linting hook configurations.
|
|
52957
53387
|
* Validates GitHub Copilot and Claude Code hook config files.
|
|
52958
53388
|
*/
|
|
52959
53389
|
|
|
52960
53390
|
|
|
52961
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
|
+
}
|
|
52962
53395
|
/**
|
|
52963
|
-
*
|
|
52964
|
-
|
|
52965
|
-
|
|
52966
|
-
|
|
52967
|
-
|
|
52968
|
-
|
|
52969
|
-
*
|
|
52970
|
-
*/
|
|
52971
|
-
|
|
52972
|
-
|
|
52973
|
-
|
|
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
|
+
}
|
|
52974
53431
|
/**
|
|
52975
|
-
*
|
|
52976
|
-
|
|
52977
|
-
|
|
52978
|
-
|
|
52979
|
-
|
|
52980
|
-
|
|
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
|
+
}
|
|
52981
53457
|
/**
|
|
52982
53458
|
* Use case for linting hook configuration files across a repository.
|
|
52983
53459
|
*/ class LintHookConfigUseCase {
|
|
@@ -53079,34 +53555,177 @@ const INVALID_JSON_MESSAGE_PREFIX = "Invalid JSON in hook configuration file";
|
|
|
53079
53555
|
return diagnostics;
|
|
53080
53556
|
}
|
|
53081
53557
|
validateClaudeConfig(parsed) {
|
|
53082
|
-
const diagnostics = [];
|
|
53083
53558
|
const result = ClaudeHooksConfigSchema.safeParse(parsed);
|
|
53084
53559
|
if (!result.success) {
|
|
53085
|
-
|
|
53086
|
-
const lastPathSegment = issue.path.length > 0 ? issue.path[issue.path.length - 1] : undefined;
|
|
53087
|
-
const isMissingCommand = lastPathSegment === "command" && (issue.code === "too_small" || issue.code === "invalid_type");
|
|
53088
|
-
diagnostics.push({
|
|
53089
|
-
line: 1,
|
|
53090
|
-
severity: "error",
|
|
53091
|
-
message: issue.message,
|
|
53092
|
-
field: issue.path.length > 0 ? issue.path.join(".") : undefined,
|
|
53093
|
-
ruleId: isMissingCommand ? "hook/missing-command" : "hook/invalid-config"
|
|
53094
|
-
});
|
|
53095
|
-
}
|
|
53096
|
-
return diagnostics;
|
|
53560
|
+
return result.error.issues.flatMap((issue)=>toClaudeDiagnostics(issue));
|
|
53097
53561
|
}
|
|
53098
|
-
|
|
53099
|
-
|
|
53100
|
-
|
|
53101
|
-
|
|
53102
|
-
|
|
53103
|
-
|
|
53104
|
-
|
|
53105
|
-
|
|
53106
|
-
|
|
53562
|
+
return collectMissingMatcherWarnings(result.data.hooks);
|
|
53563
|
+
}
|
|
53564
|
+
}
|
|
53565
|
+
|
|
53566
|
+
;// CONCATENATED MODULE: ../core/src/use-cases/lint-mcp-servers.ts
|
|
53567
|
+
/**
|
|
53568
|
+
* Use case for linting MCP server configuration files.
|
|
53569
|
+
* Validates baseline shape: the config parses as JSON and, when present,
|
|
53570
|
+
* `mcpServers` (Claude Code's `.mcp.json` convention) or `servers` (VS
|
|
53571
|
+
* Code's `.vscode/mcp.json` convention) is a map of server name to a
|
|
53572
|
+
* server declaration.
|
|
53573
|
+
*/
|
|
53574
|
+
const lint_mcp_servers_INVALID_JSON_MESSAGE_PREFIX = "Invalid JSON in MCP server configuration file";
|
|
53575
|
+
/**
|
|
53576
|
+
* Property names that must never be used as MCP server names: assigning
|
|
53577
|
+
* them via a plain object key (as zod's `record()` does when reconstructing
|
|
53578
|
+
* parsed output) triggers JavaScript's `__proto__` magic setter instead of
|
|
53579
|
+
* creating an own property, silently dropping the entry from validation and
|
|
53580
|
+
* counts. Rejected explicitly so a config declaring one of these names
|
|
53581
|
+
* fails lint instead of appearing to declare fewer servers than it does.
|
|
53582
|
+
*/ const lint_mcp_servers_RESERVED_SERVER_NAMES = [
|
|
53583
|
+
"__proto__",
|
|
53584
|
+
"constructor",
|
|
53585
|
+
"prototype"
|
|
53586
|
+
];
|
|
53587
|
+
const lint_mcp_servers_SERVER_MAP_FIELDS = [
|
|
53588
|
+
"mcpServers",
|
|
53589
|
+
"servers"
|
|
53590
|
+
];
|
|
53591
|
+
/**
|
|
53592
|
+
* Zod schema for a single MCP server declaration.
|
|
53593
|
+
* Baseline shape only: an object whose optional `type`/`transport` fields,
|
|
53594
|
+
* when present, must be non-empty strings. Unknown fields are preserved.
|
|
53595
|
+
*/ const lint_mcp_servers_McpServerEntrySchema = schemas_object({
|
|
53596
|
+
type: schemas_string().min(1, "type must not be empty").optional(),
|
|
53597
|
+
transport: schemas_string().min(1, "transport must not be empty").optional()
|
|
53598
|
+
}).passthrough();
|
|
53599
|
+
/**
|
|
53600
|
+
* Zod schema for the MCP config file shape.
|
|
53601
|
+
*/ const lint_mcp_servers_McpConfigSchema = schemas_object({
|
|
53602
|
+
mcpServers: schemas_record(schemas_string(), lint_mcp_servers_McpServerEntrySchema).optional(),
|
|
53603
|
+
servers: schemas_record(schemas_string(), lint_mcp_servers_McpServerEntrySchema).optional()
|
|
53604
|
+
}).passthrough();
|
|
53605
|
+
function lint_mcp_servers_isPlainObject(value) {
|
|
53606
|
+
return typeof value === "object" && value !== null;
|
|
53607
|
+
}
|
|
53608
|
+
function reservedNameDiagnostic(field, reservedName) {
|
|
53609
|
+
return {
|
|
53610
|
+
line: 1,
|
|
53611
|
+
severity: "error",
|
|
53612
|
+
message: `Server name '${reservedName}' is a reserved JavaScript property name and is not allowed.`,
|
|
53613
|
+
field: `${field}.${reservedName}`,
|
|
53614
|
+
ruleId: "mcpserver/invalid-config"
|
|
53615
|
+
};
|
|
53616
|
+
}
|
|
53617
|
+
function reservedNamesInServerMap(servers, field) {
|
|
53618
|
+
const diagnostics = [];
|
|
53619
|
+
for (const reservedName of lint_mcp_servers_RESERVED_SERVER_NAMES){
|
|
53620
|
+
if (Object.hasOwn(servers, reservedName)) {
|
|
53621
|
+
diagnostics.push(reservedNameDiagnostic(field, reservedName));
|
|
53622
|
+
}
|
|
53623
|
+
}
|
|
53624
|
+
return diagnostics;
|
|
53625
|
+
}
|
|
53626
|
+
/**
|
|
53627
|
+
* Scans the raw (pre-zod) parsed config for reserved server names. Must run
|
|
53628
|
+
* against the raw `JSON.parse` output, which preserves `__proto__` as an
|
|
53629
|
+
* ordinary own property, rather than against zod's reconstructed output.
|
|
53630
|
+
*/ function findReservedServerNameDiagnostics(parsed) {
|
|
53631
|
+
if (!lint_mcp_servers_isPlainObject(parsed)) {
|
|
53632
|
+
return [];
|
|
53633
|
+
}
|
|
53634
|
+
const diagnostics = [];
|
|
53635
|
+
for (const field of lint_mcp_servers_SERVER_MAP_FIELDS){
|
|
53636
|
+
const servers = parsed[field];
|
|
53637
|
+
if (!lint_mcp_servers_isPlainObject(servers)) {
|
|
53638
|
+
continue;
|
|
53639
|
+
}
|
|
53640
|
+
diagnostics.push(...reservedNamesInServerMap(servers, field));
|
|
53641
|
+
}
|
|
53642
|
+
return diagnostics;
|
|
53643
|
+
}
|
|
53644
|
+
function invalidJsonResult(filePath, error) {
|
|
53645
|
+
const detail = error instanceof Error && error.message ? `: ${error.message}` : ".";
|
|
53646
|
+
return {
|
|
53647
|
+
filePath,
|
|
53648
|
+
serverCount: 0,
|
|
53649
|
+
diagnostics: [
|
|
53650
|
+
{
|
|
53651
|
+
line: 1,
|
|
53652
|
+
severity: "error",
|
|
53653
|
+
message: `${lint_mcp_servers_INVALID_JSON_MESSAGE_PREFIX}${detail}`,
|
|
53654
|
+
ruleId: "mcpserver/invalid-json"
|
|
53107
53655
|
}
|
|
53656
|
+
],
|
|
53657
|
+
valid: false
|
|
53658
|
+
};
|
|
53659
|
+
}
|
|
53660
|
+
function invalidConfigResult(filePath, diagnostics) {
|
|
53661
|
+
return {
|
|
53662
|
+
filePath,
|
|
53663
|
+
serverCount: 0,
|
|
53664
|
+
diagnostics,
|
|
53665
|
+
valid: false
|
|
53666
|
+
};
|
|
53667
|
+
}
|
|
53668
|
+
function zodIssuesToDiagnostics(issues) {
|
|
53669
|
+
return issues.map((issue)=>({
|
|
53670
|
+
line: 1,
|
|
53671
|
+
severity: "error",
|
|
53672
|
+
message: issue.message,
|
|
53673
|
+
field: issue.path.length > 0 ? issue.path.join(".") : undefined,
|
|
53674
|
+
ruleId: "mcpserver/invalid-config"
|
|
53675
|
+
}));
|
|
53676
|
+
}
|
|
53677
|
+
function countBySeverity(results, severity) {
|
|
53678
|
+
return results.reduce((sum, r)=>sum + r.diagnostics.filter((d)=>d.severity === severity).length, 0);
|
|
53679
|
+
}
|
|
53680
|
+
/**
|
|
53681
|
+
* Use case for linting MCP server configuration files across a repository.
|
|
53682
|
+
*/ class LintMcpServersUseCase {
|
|
53683
|
+
gateway;
|
|
53684
|
+
constructor(gateway){
|
|
53685
|
+
this.gateway = gateway;
|
|
53686
|
+
}
|
|
53687
|
+
async execute(input) {
|
|
53688
|
+
if (!input.targetDir) {
|
|
53689
|
+
throw new Error("Target directory is required");
|
|
53108
53690
|
}
|
|
53109
|
-
|
|
53691
|
+
const files = await this.gateway.discoverMcpConfigFiles(input.targetDir);
|
|
53692
|
+
const results = [];
|
|
53693
|
+
for (const file of files){
|
|
53694
|
+
const content = await this.gateway.readFileContent(file.filePath);
|
|
53695
|
+
results.push(this.lintFile(file, content));
|
|
53696
|
+
}
|
|
53697
|
+
return {
|
|
53698
|
+
results,
|
|
53699
|
+
totalFiles: files.length,
|
|
53700
|
+
totalErrors: countBySeverity(results, "error"),
|
|
53701
|
+
totalWarnings: countBySeverity(results, "warning")
|
|
53702
|
+
};
|
|
53703
|
+
}
|
|
53704
|
+
lintFile(file, content) {
|
|
53705
|
+
let parsed;
|
|
53706
|
+
try {
|
|
53707
|
+
parsed = JSON.parse(content);
|
|
53708
|
+
} catch (error) {
|
|
53709
|
+
return invalidJsonResult(file.filePath, error);
|
|
53710
|
+
}
|
|
53711
|
+
const reservedNameDiagnostics = findReservedServerNameDiagnostics(parsed);
|
|
53712
|
+
if (reservedNameDiagnostics.length > 0) {
|
|
53713
|
+
return invalidConfigResult(file.filePath, reservedNameDiagnostics);
|
|
53714
|
+
}
|
|
53715
|
+
const result = lint_mcp_servers_McpConfigSchema.safeParse(parsed);
|
|
53716
|
+
if (!result.success) {
|
|
53717
|
+
return invalidConfigResult(file.filePath, zodIssuesToDiagnostics(result.error.issues));
|
|
53718
|
+
}
|
|
53719
|
+
const servers = {
|
|
53720
|
+
...result.data.mcpServers ?? {},
|
|
53721
|
+
...result.data.servers ?? {}
|
|
53722
|
+
};
|
|
53723
|
+
return {
|
|
53724
|
+
filePath: file.filePath,
|
|
53725
|
+
serverCount: Object.keys(servers).length,
|
|
53726
|
+
diagnostics: [],
|
|
53727
|
+
valid: true
|
|
53728
|
+
};
|
|
53110
53729
|
}
|
|
53111
53730
|
}
|
|
53112
53731
|
|
|
@@ -53265,6 +53884,138 @@ function hasFrontmatterDelimiters(content) {
|
|
|
53265
53884
|
return false;
|
|
53266
53885
|
}
|
|
53267
53886
|
|
|
53887
|
+
;// CONCATENATED MODULE: ../core/src/use-cases/lint-subagent-frontmatter.ts
|
|
53888
|
+
/**
|
|
53889
|
+
* Use case for linting Claude Code subagent frontmatter.
|
|
53890
|
+
* Validates required fields, name format, and filename matching.
|
|
53891
|
+
*/
|
|
53892
|
+
/**
|
|
53893
|
+
* Zod schema for validating subagent frontmatter.
|
|
53894
|
+
* Claude Code subagents require `name` and `description`, mirroring the
|
|
53895
|
+
* Copilot custom agent contract (see AgentFrontmatterSchema).
|
|
53896
|
+
*/ const SubagentFrontmatterSchema = schemas_object({
|
|
53897
|
+
name: schemas_string().min(1, "Name is required").max(64, "Name must be 64 characters or fewer").regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Name must contain only lowercase letters, numbers, and hyphens"),
|
|
53898
|
+
description: schemas_string().min(1, "Description is required").max(1024, "Description must be 1024 characters or fewer").refine((s)=>s.trim().length > 0, {
|
|
53899
|
+
message: "Description cannot be empty or whitespace-only"
|
|
53900
|
+
})
|
|
53901
|
+
});
|
|
53902
|
+
/**
|
|
53903
|
+
* Use case for linting subagent frontmatter across a repository.
|
|
53904
|
+
*/ class LintSubagentFrontmatterUseCase {
|
|
53905
|
+
gateway;
|
|
53906
|
+
constructor(gateway){
|
|
53907
|
+
this.gateway = gateway;
|
|
53908
|
+
}
|
|
53909
|
+
async execute(input) {
|
|
53910
|
+
if (!input.targetDir) {
|
|
53911
|
+
throw new Error("Target directory is required");
|
|
53912
|
+
}
|
|
53913
|
+
const subagents = await this.gateway.discoverSubagents(input.targetDir);
|
|
53914
|
+
const results = [];
|
|
53915
|
+
for (const subagent of subagents){
|
|
53916
|
+
const content = await this.gateway.readSubagentFileContent(subagent.filePath);
|
|
53917
|
+
const result = this.lintSubagent(subagent, content);
|
|
53918
|
+
results.push(result);
|
|
53919
|
+
}
|
|
53920
|
+
const totalErrors = results.reduce((sum, r)=>sum + r.diagnostics.filter((d)=>d.severity === "error").length, 0);
|
|
53921
|
+
const totalWarnings = results.reduce((sum, r)=>sum + r.diagnostics.filter((d)=>d.severity === "warning").length, 0);
|
|
53922
|
+
return {
|
|
53923
|
+
results,
|
|
53924
|
+
totalSubagents: subagents.length,
|
|
53925
|
+
totalErrors,
|
|
53926
|
+
totalWarnings
|
|
53927
|
+
};
|
|
53928
|
+
}
|
|
53929
|
+
lintSubagent(subagent, content) {
|
|
53930
|
+
let parsed = null;
|
|
53931
|
+
let diagnostics = [];
|
|
53932
|
+
try {
|
|
53933
|
+
parsed = this.gateway.parseFrontmatter(content);
|
|
53934
|
+
} catch (error) {
|
|
53935
|
+
const messagePrefix = "Invalid YAML frontmatter";
|
|
53936
|
+
const errorMessage = error instanceof Error && error.message ? `${messagePrefix}: ${error.message}` : `${messagePrefix}.`;
|
|
53937
|
+
diagnostics.push({
|
|
53938
|
+
line: 1,
|
|
53939
|
+
severity: "error",
|
|
53940
|
+
message: errorMessage,
|
|
53941
|
+
ruleId: "subagent/invalid-frontmatter"
|
|
53942
|
+
});
|
|
53943
|
+
}
|
|
53944
|
+
if (!parsed) {
|
|
53945
|
+
if (diagnostics.length === 0) {
|
|
53946
|
+
const message = lint_subagent_frontmatter_hasOpeningDelimiter(content) ? "Invalid YAML frontmatter. The content between --- delimiters could not be parsed as valid YAML." : "Missing YAML frontmatter. Subagent files must begin with --- delimited YAML frontmatter.";
|
|
53947
|
+
const ruleId = lint_subagent_frontmatter_hasOpeningDelimiter(content) ? "subagent/invalid-frontmatter" : "subagent/missing-frontmatter";
|
|
53948
|
+
diagnostics.push({
|
|
53949
|
+
line: 1,
|
|
53950
|
+
severity: "error",
|
|
53951
|
+
message,
|
|
53952
|
+
ruleId
|
|
53953
|
+
});
|
|
53954
|
+
}
|
|
53955
|
+
return {
|
|
53956
|
+
filePath: subagent.filePath,
|
|
53957
|
+
subagentName: subagent.subagentName,
|
|
53958
|
+
diagnostics,
|
|
53959
|
+
valid: false
|
|
53960
|
+
};
|
|
53961
|
+
}
|
|
53962
|
+
const frontmatterDiagnostics = this.validateFrontmatter(parsed, subagent.subagentName);
|
|
53963
|
+
diagnostics = diagnostics.concat(frontmatterDiagnostics);
|
|
53964
|
+
return {
|
|
53965
|
+
filePath: subagent.filePath,
|
|
53966
|
+
subagentName: subagent.subagentName,
|
|
53967
|
+
diagnostics,
|
|
53968
|
+
valid: diagnostics.every((d)=>d.severity !== "error")
|
|
53969
|
+
};
|
|
53970
|
+
}
|
|
53971
|
+
validateFrontmatter(parsed, filenameStem) {
|
|
53972
|
+
const diagnostics = [];
|
|
53973
|
+
const result = SubagentFrontmatterSchema.safeParse(parsed.data);
|
|
53974
|
+
if (!result.success) {
|
|
53975
|
+
for (const issue of result.error.issues){
|
|
53976
|
+
const fieldName = issue.path[0]?.toString();
|
|
53977
|
+
const line = fieldName ? parsed.fieldLines.get(fieldName) ?? parsed.frontmatterStartLine : parsed.frontmatterStartLine;
|
|
53978
|
+
const isMissing = fieldName !== undefined && parsed.data[fieldName] === undefined;
|
|
53979
|
+
const ruleId = this.getRuleIdForField(fieldName, isMissing);
|
|
53980
|
+
diagnostics.push({
|
|
53981
|
+
line,
|
|
53982
|
+
severity: "error",
|
|
53983
|
+
message: issue.message,
|
|
53984
|
+
field: fieldName,
|
|
53985
|
+
ruleId
|
|
53986
|
+
});
|
|
53987
|
+
}
|
|
53988
|
+
}
|
|
53989
|
+
// Check name matches filename stem
|
|
53990
|
+
if (result.success && result.data.name !== filenameStem) {
|
|
53991
|
+
const nameLine = parsed.fieldLines.get("name") ?? parsed.frontmatterStartLine;
|
|
53992
|
+
diagnostics.push({
|
|
53993
|
+
line: nameLine,
|
|
53994
|
+
severity: "error",
|
|
53995
|
+
message: `Frontmatter name '${result.data.name}' does not match filename '${filenameStem}'. Claude Code identifies subagents by the name field, not the filename, but matching them is a helpful convention.`,
|
|
53996
|
+
field: "name",
|
|
53997
|
+
ruleId: "subagent/name-mismatch"
|
|
53998
|
+
});
|
|
53999
|
+
}
|
|
54000
|
+
return diagnostics;
|
|
54001
|
+
}
|
|
54002
|
+
getRuleIdForField(fieldName, isMissing) {
|
|
54003
|
+
if (fieldName === "name") {
|
|
54004
|
+
return isMissing ? "subagent/missing-name" : "subagent/invalid-name-format";
|
|
54005
|
+
}
|
|
54006
|
+
if (fieldName === "description") {
|
|
54007
|
+
return isMissing ? "subagent/missing-description" : "subagent/invalid-description";
|
|
54008
|
+
}
|
|
54009
|
+
return "subagent/invalid-field";
|
|
54010
|
+
}
|
|
54011
|
+
}
|
|
54012
|
+
/**
|
|
54013
|
+
* Checks whether content has an opening --- frontmatter delimiter.
|
|
54014
|
+
*/ function lint_subagent_frontmatter_hasOpeningDelimiter(content) {
|
|
54015
|
+
const lines = content.split("\n");
|
|
54016
|
+
return lines[0]?.trim() === "---";
|
|
54017
|
+
}
|
|
54018
|
+
|
|
53268
54019
|
;// CONCATENATED MODULE: ../lint/src/lint-errors.ts
|
|
53269
54020
|
/**
|
|
53270
54021
|
* Public lint API error types.
|
|
@@ -53405,11 +54156,17 @@ function sanitizeForErrorMessage(value) {
|
|
|
53405
54156
|
|
|
53406
54157
|
|
|
53407
54158
|
|
|
54159
|
+
|
|
54160
|
+
|
|
54161
|
+
|
|
54162
|
+
|
|
53408
54163
|
const LintTargetsSchema = schemas_object({
|
|
53409
54164
|
skills: schemas_boolean().optional(),
|
|
53410
54165
|
agents: schemas_boolean().optional(),
|
|
54166
|
+
subagents: schemas_boolean().optional(),
|
|
53411
54167
|
hooks: schemas_boolean().optional(),
|
|
53412
|
-
instructions: schemas_boolean().optional()
|
|
54168
|
+
instructions: schemas_boolean().optional(),
|
|
54169
|
+
mcpServers: schemas_boolean().optional()
|
|
53413
54170
|
}).strict().optional();
|
|
53414
54171
|
const LintOptionsSchema = schemas_object({
|
|
53415
54172
|
directory: schemas_string().min(1, "directory must not be empty"),
|
|
@@ -53465,6 +54222,22 @@ async function lintAgents(targetDir) {
|
|
|
53465
54222
|
});
|
|
53466
54223
|
return toLintOutput(output, "agent", output.totalAgents);
|
|
53467
54224
|
}
|
|
54225
|
+
async function lintSubagents(targetDir) {
|
|
54226
|
+
const gateway = createSubagentLintGateway();
|
|
54227
|
+
const useCase = new LintSubagentFrontmatterUseCase(gateway);
|
|
54228
|
+
const output = await useCase.execute({
|
|
54229
|
+
targetDir
|
|
54230
|
+
});
|
|
54231
|
+
return toLintOutput(output, "subagent", output.totalSubagents);
|
|
54232
|
+
}
|
|
54233
|
+
async function lintMcpServers(targetDir) {
|
|
54234
|
+
const gateway = createMcpServersLintGateway();
|
|
54235
|
+
const useCase = new LintMcpServersUseCase(gateway);
|
|
54236
|
+
const output = await useCase.execute({
|
|
54237
|
+
targetDir
|
|
54238
|
+
});
|
|
54239
|
+
return toLintOutput(output, "mcp-server", output.totalFiles);
|
|
54240
|
+
}
|
|
53468
54241
|
async function lintHooks(targetDir) {
|
|
53469
54242
|
const gateway = createHookConfigGateway();
|
|
53470
54243
|
const useCase = new LintHookConfigUseCase(gateway);
|
|
@@ -53495,10 +54268,15 @@ async function lintInstructions(targetDir, logger) {
|
|
|
53495
54268
|
}
|
|
53496
54269
|
};
|
|
53497
54270
|
}
|
|
53498
|
-
function isTargetEnabled(
|
|
53499
|
-
if (!targets)
|
|
54271
|
+
function isTargetEnabled(target, targets) {
|
|
54272
|
+
if (!targets) {
|
|
54273
|
+
return target.defaultEnabled;
|
|
54274
|
+
}
|
|
53500
54275
|
const hasAnyEnabled = Object.values(targets).some(Boolean);
|
|
53501
|
-
|
|
54276
|
+
if (!hasAnyEnabled) {
|
|
54277
|
+
return target.defaultEnabled;
|
|
54278
|
+
}
|
|
54279
|
+
return targets[target.key] === true;
|
|
53502
54280
|
}
|
|
53503
54281
|
/**
|
|
53504
54282
|
* Run lint checks on a project directory.
|
|
@@ -53506,7 +54284,10 @@ function isTargetEnabled(key, targets) {
|
|
|
53506
54284
|
* Orchestrates all lint targets (skills, agents, hooks, instructions),
|
|
53507
54285
|
* applies lint rule configuration, and returns structured results.
|
|
53508
54286
|
*
|
|
53509
|
-
* When no targets are specified (or all are false),
|
|
54287
|
+
* When no targets are specified (or all are false), the default-enabled
|
|
54288
|
+
* targets are run (skills, agents, hooks, instructions). `subagents` and
|
|
54289
|
+
* `mcpServers` are flag-only (not yet GA): they only run when explicitly
|
|
54290
|
+
* set to `true`, even when no other target flags are set.
|
|
53510
54291
|
*
|
|
53511
54292
|
* @example
|
|
53512
54293
|
* ```typescript
|
|
@@ -53541,22 +54322,36 @@ function isTargetEnabled(key, targets) {
|
|
|
53541
54322
|
const lintTargets = [
|
|
53542
54323
|
{
|
|
53543
54324
|
key: "skills",
|
|
54325
|
+
defaultEnabled: true,
|
|
53544
54326
|
execute: lintSkills
|
|
53545
54327
|
},
|
|
53546
54328
|
{
|
|
53547
54329
|
key: "agents",
|
|
54330
|
+
defaultEnabled: true,
|
|
53548
54331
|
execute: lintAgents
|
|
53549
54332
|
},
|
|
54333
|
+
{
|
|
54334
|
+
key: "subagents",
|
|
54335
|
+
defaultEnabled: false,
|
|
54336
|
+
execute: lintSubagents
|
|
54337
|
+
},
|
|
53550
54338
|
{
|
|
53551
54339
|
key: "hooks",
|
|
54340
|
+
defaultEnabled: true,
|
|
53552
54341
|
execute: lintHooks
|
|
53553
54342
|
},
|
|
53554
54343
|
{
|
|
53555
54344
|
key: "instructions",
|
|
54345
|
+
defaultEnabled: true,
|
|
53556
54346
|
execute: (dir)=>lintInstructions(dir, parsed.logger)
|
|
54347
|
+
},
|
|
54348
|
+
{
|
|
54349
|
+
key: "mcpServers",
|
|
54350
|
+
defaultEnabled: false,
|
|
54351
|
+
execute: lintMcpServers
|
|
53557
54352
|
}
|
|
53558
54353
|
];
|
|
53559
|
-
const enabledTargets = lintTargets.filter((t)=>isTargetEnabled(t
|
|
54354
|
+
const enabledTargets = lintTargets.filter((t)=>isTargetEnabled(t, parsed.targets));
|
|
53560
54355
|
const outputs = [];
|
|
53561
54356
|
for (const target of enabledTargets){
|
|
53562
54357
|
const rawOutput = await target.execute(targetDir);
|
|
@@ -53591,61 +54386,225 @@ function isTargetEnabled(key, targets) {
|
|
|
53591
54386
|
|
|
53592
54387
|
|
|
53593
54388
|
|
|
53594
|
-
;// CONCATENATED MODULE: ./src/commands/lint.ts
|
|
54389
|
+
;// CONCATENATED MODULE: ./src/commands/lint-instruction-display.ts
|
|
53595
54390
|
/**
|
|
53596
|
-
*
|
|
53597
|
-
* Delegates to the lint package facade and handles CLI display concerns.
|
|
54391
|
+
* Instruction-quality display for the lint CLI command.
|
|
53598
54392
|
*/
|
|
54393
|
+
function warnSuggestions(suggestions) {
|
|
54394
|
+
for (const suggestion of suggestions){
|
|
54395
|
+
consola.warn(suggestion.message);
|
|
54396
|
+
}
|
|
54397
|
+
}
|
|
54398
|
+
/**
|
|
54399
|
+
* Displays instruction quality analysis results using consola.
|
|
54400
|
+
*/ function displayInstructionQuality(output) {
|
|
54401
|
+
const result = output.qualityResult;
|
|
54402
|
+
if (!result) {
|
|
54403
|
+
return;
|
|
54404
|
+
}
|
|
54405
|
+
if (result.discoveredFiles.length === 0) {
|
|
54406
|
+
consola.info("No instruction files found");
|
|
54407
|
+
warnSuggestions(result.suggestions);
|
|
54408
|
+
return;
|
|
54409
|
+
}
|
|
54410
|
+
consola.info(`Discovered ${result.discoveredFiles.length} instruction file(s)`);
|
|
54411
|
+
for (const file of result.discoveredFiles){
|
|
54412
|
+
consola.info(` ${file.filePath} (${file.format})`);
|
|
54413
|
+
}
|
|
54414
|
+
consola.info(`Overall instruction quality score: ${result.overallQualityScore}%`);
|
|
54415
|
+
warnSuggestions(result.suggestions);
|
|
54416
|
+
}
|
|
53599
54417
|
|
|
54418
|
+
;// CONCATENATED MODULE: ./src/commands/lint-target-labels.ts
|
|
54419
|
+
/**
|
|
54420
|
+
* Shared human-readable labels for lint target categories, used by both
|
|
54421
|
+
* per-file diagnostic display and the pass/fail summary line.
|
|
54422
|
+
*/ const TARGET_LABELS = {
|
|
54423
|
+
skill: "skill(s)",
|
|
54424
|
+
agent: "agent(s)",
|
|
54425
|
+
subagent: "subagent(s)",
|
|
54426
|
+
hook: "hook config(s)",
|
|
54427
|
+
instruction: "instruction file(s)",
|
|
54428
|
+
"mcp-server": "MCP server config(s)"
|
|
54429
|
+
};
|
|
54430
|
+
function targetLabel(target) {
|
|
54431
|
+
return TARGET_LABELS[target] ?? target;
|
|
54432
|
+
}
|
|
53600
54433
|
|
|
54434
|
+
;// CONCATENATED MODULE: ./src/commands/lint-display.ts
|
|
53601
54435
|
/**
|
|
53602
|
-
*
|
|
53603
|
-
*/
|
|
54436
|
+
* Human-readable display helpers for the lint CLI command.
|
|
54437
|
+
*/
|
|
54438
|
+
|
|
54439
|
+
|
|
54440
|
+
|
|
54441
|
+
const SEVERITY_LOGGERS = {
|
|
54442
|
+
error: (msg)=>consola.error(msg),
|
|
54443
|
+
warning: (msg)=>consola.warn(msg),
|
|
54444
|
+
info: (msg)=>consola.info(msg)
|
|
54445
|
+
};
|
|
54446
|
+
function logDiagnostic(d) {
|
|
54447
|
+
const fieldInfo = d.field ? ` [${d.field}]` : "";
|
|
54448
|
+
const message = `${d.filePath}:${d.line}${fieldInfo}: ${d.message}`;
|
|
54449
|
+
const log = SEVERITY_LOGGERS[d.severity] ?? SEVERITY_LOGGERS.info;
|
|
54450
|
+
log(message);
|
|
54451
|
+
}
|
|
54452
|
+
function displayOkFiles(output) {
|
|
54453
|
+
const filesWithDiagnostics = new Set(output.diagnostics.map((d)=>d.filePath));
|
|
54454
|
+
for (const file of output.filesAnalyzed){
|
|
54455
|
+
if (filesWithDiagnostics.has(file)) {
|
|
54456
|
+
continue;
|
|
54457
|
+
}
|
|
54458
|
+
consola.success(`${file}: OK`);
|
|
54459
|
+
}
|
|
54460
|
+
}
|
|
54461
|
+
function displayLintOutput(output, label) {
|
|
53604
54462
|
if (output.summary.totalFiles === 0) {
|
|
53605
54463
|
consola.info(`No ${label} found`);
|
|
53606
54464
|
return;
|
|
53607
54465
|
}
|
|
53608
54466
|
consola.info(`Discovered ${output.summary.totalFiles} ${label}`);
|
|
53609
|
-
|
|
54467
|
+
displayOkFiles(output);
|
|
53610
54468
|
for (const d of output.diagnostics){
|
|
53611
|
-
|
|
54469
|
+
logDiagnostic(d);
|
|
53612
54470
|
}
|
|
53613
|
-
|
|
53614
|
-
|
|
53615
|
-
|
|
54471
|
+
}
|
|
54472
|
+
function displayHumanOutputs(outputs) {
|
|
54473
|
+
for (const output of outputs){
|
|
54474
|
+
if (output.target === "instruction") {
|
|
54475
|
+
displayInstructionQuality(output);
|
|
54476
|
+
continue;
|
|
53616
54477
|
}
|
|
54478
|
+
displayLintOutput(output, targetLabel(output.target));
|
|
53617
54479
|
}
|
|
53618
|
-
|
|
53619
|
-
|
|
53620
|
-
|
|
53621
|
-
|
|
53622
|
-
|
|
53623
|
-
} else if (d.severity === "warning") {
|
|
53624
|
-
consola.warn(`${prefix}${fieldInfo}: ${d.message}`);
|
|
53625
|
-
} else {
|
|
53626
|
-
consola.info(`${prefix}${fieldInfo}: ${d.message}`);
|
|
53627
|
-
}
|
|
54480
|
+
}
|
|
54481
|
+
function displayFormattedOutputs(outputs, format) {
|
|
54482
|
+
if (format === "human") {
|
|
54483
|
+
displayHumanOutputs(outputs);
|
|
54484
|
+
return;
|
|
53628
54485
|
}
|
|
54486
|
+
const formatted = createFormatter(format).format(outputs);
|
|
54487
|
+
if (!formatted) {
|
|
54488
|
+
return;
|
|
54489
|
+
}
|
|
54490
|
+
process.stdout.write(`${formatted}\n`);
|
|
54491
|
+
}
|
|
54492
|
+
function sumWarnings(outputs) {
|
|
54493
|
+
return outputs.reduce((sum, output)=>sum + output.summary.totalWarnings, 0);
|
|
53629
54494
|
}
|
|
54495
|
+
|
|
54496
|
+
;// CONCATENATED MODULE: ./src/commands/lint-report.ts
|
|
53630
54497
|
/**
|
|
53631
|
-
*
|
|
53632
|
-
*/
|
|
53633
|
-
|
|
53634
|
-
|
|
54498
|
+
* Pass/fail summary reporting for the lint CLI command.
|
|
54499
|
+
*/
|
|
54500
|
+
|
|
54501
|
+
function reportFailure(outputs, totalWarnings, format) {
|
|
54502
|
+
process.exitCode = 1;
|
|
54503
|
+
if (format !== "human") {
|
|
53635
54504
|
return;
|
|
53636
54505
|
}
|
|
53637
|
-
|
|
53638
|
-
|
|
53639
|
-
|
|
53640
|
-
|
|
53641
|
-
|
|
53642
|
-
|
|
54506
|
+
const totalErrors = outputs.reduce((sum, o)=>sum + o.summary.totalErrors, 0);
|
|
54507
|
+
consola.error(`lint failed: ${totalErrors} error(s), ${totalWarnings} warning(s)`);
|
|
54508
|
+
}
|
|
54509
|
+
function reportSuccess(outputs, totalWarnings, format) {
|
|
54510
|
+
if (format !== "human") {
|
|
54511
|
+
return;
|
|
54512
|
+
}
|
|
54513
|
+
if (totalWarnings > 0) {
|
|
54514
|
+
consola.warn(`Lint passed with ${totalWarnings} warning(s)`);
|
|
54515
|
+
return;
|
|
54516
|
+
}
|
|
54517
|
+
const targets = outputs.map((o)=>targetLabel(o.target)).join(", ");
|
|
54518
|
+
consola.success(`All ${targets} passed lint checks`);
|
|
54519
|
+
}
|
|
54520
|
+
|
|
54521
|
+
;// CONCATENATED MODULE: ./src/commands/lint.ts
|
|
54522
|
+
/**
|
|
54523
|
+
* CLI command for linting agent skills, custom agents, and instruction files.
|
|
54524
|
+
* Delegates to the lint package facade and handles CLI display concerns.
|
|
54525
|
+
*/
|
|
54526
|
+
|
|
54527
|
+
|
|
54528
|
+
|
|
54529
|
+
|
|
54530
|
+
const VALID_FORMATS = new Set([
|
|
54531
|
+
"human",
|
|
54532
|
+
"json",
|
|
54533
|
+
"rdjsonl"
|
|
54534
|
+
]);
|
|
54535
|
+
function resolveTargetDir(context) {
|
|
54536
|
+
if (typeof context.data?.targetDir === "string") {
|
|
54537
|
+
return context.data.targetDir;
|
|
54538
|
+
}
|
|
54539
|
+
return process.cwd();
|
|
54540
|
+
}
|
|
54541
|
+
function isLessonsSubcommand(rawArgs) {
|
|
54542
|
+
return rawArgs.some((token, idx)=>token === "lessons" && rawArgs[idx - 1] !== "--format");
|
|
54543
|
+
}
|
|
54544
|
+
function resolveBooleanFlag(context, key) {
|
|
54545
|
+
return context.args?.[key] === true || context.data?.[key] === true;
|
|
54546
|
+
}
|
|
54547
|
+
function isLintFormatType(value) {
|
|
54548
|
+
return VALID_FORMATS.has(value);
|
|
54549
|
+
}
|
|
54550
|
+
function readRawFormat(context) {
|
|
54551
|
+
if (typeof context.args?.format === "string") {
|
|
54552
|
+
return context.args.format;
|
|
54553
|
+
}
|
|
54554
|
+
if (typeof context.data?.format === "string") {
|
|
54555
|
+
return context.data.format;
|
|
54556
|
+
}
|
|
54557
|
+
return "human";
|
|
54558
|
+
}
|
|
54559
|
+
function resolveFormat(context) {
|
|
54560
|
+
const rawFormat = readRawFormat(context);
|
|
54561
|
+
if (isLintFormatType(rawFormat)) {
|
|
54562
|
+
return rawFormat;
|
|
54563
|
+
}
|
|
54564
|
+
return "human";
|
|
54565
|
+
}
|
|
54566
|
+
function resolveLintTargets(context) {
|
|
54567
|
+
return {
|
|
54568
|
+
skills: resolveBooleanFlag(context, "skills"),
|
|
54569
|
+
agents: resolveBooleanFlag(context, "agents"),
|
|
54570
|
+
subagents: resolveBooleanFlag(context, "subagents"),
|
|
54571
|
+
hooks: resolveBooleanFlag(context, "hooks"),
|
|
54572
|
+
instructions: resolveBooleanFlag(context, "instructions"),
|
|
54573
|
+
mcpServers: resolveBooleanFlag(context, "mcpServers")
|
|
54574
|
+
};
|
|
54575
|
+
}
|
|
54576
|
+
async function executeLint(context) {
|
|
54577
|
+
try {
|
|
54578
|
+
return await runLint({
|
|
54579
|
+
directory: resolveTargetDir(context),
|
|
54580
|
+
targets: resolveLintTargets(context)
|
|
54581
|
+
});
|
|
54582
|
+
} catch (error) {
|
|
54583
|
+
if (error instanceof LintValidationError) {
|
|
54584
|
+
consola.error(`Lint failed: ${error.message}`);
|
|
54585
|
+
process.exitCode = 1;
|
|
54586
|
+
return null;
|
|
53643
54587
|
}
|
|
53644
|
-
|
|
54588
|
+
throw error;
|
|
53645
54589
|
}
|
|
53646
|
-
|
|
53647
|
-
|
|
54590
|
+
}
|
|
54591
|
+
async function runLintCommand(context) {
|
|
54592
|
+
if (isLessonsSubcommand(context.rawArgs ?? [])) {
|
|
54593
|
+
return;
|
|
54594
|
+
}
|
|
54595
|
+
const format = resolveFormat(context);
|
|
54596
|
+
const result = await executeLint(context);
|
|
54597
|
+
if (result === null) {
|
|
54598
|
+
return;
|
|
53648
54599
|
}
|
|
54600
|
+
const { outputs, hasErrors } = result;
|
|
54601
|
+
const totalWarnings = sumWarnings(outputs);
|
|
54602
|
+
displayFormattedOutputs(outputs, format);
|
|
54603
|
+
if (hasErrors) {
|
|
54604
|
+
reportFailure(outputs, totalWarnings, format);
|
|
54605
|
+
return;
|
|
54606
|
+
}
|
|
54607
|
+
reportSuccess(outputs, totalWarnings, format);
|
|
53649
54608
|
}
|
|
53650
54609
|
/**
|
|
53651
54610
|
* The `lint` command for validating agent skills, custom agents, and instruction files.
|
|
@@ -53661,6 +54620,11 @@ function isTargetEnabled(key, targets) {
|
|
|
53661
54620
|
description: "Lint custom agent frontmatter in .github/agents/",
|
|
53662
54621
|
default: false
|
|
53663
54622
|
},
|
|
54623
|
+
subagents: {
|
|
54624
|
+
type: "boolean",
|
|
54625
|
+
description: "[flag-only, not default-on] Lint Claude Code subagent frontmatter in .claude/agents/",
|
|
54626
|
+
default: false
|
|
54627
|
+
},
|
|
53664
54628
|
hooks: {
|
|
53665
54629
|
type: "boolean",
|
|
53666
54630
|
description: "Lint pre-tool-use hook configurations in .github/hooks/agent-shell/hooks.json, .claude/settings.json, and .claude/settings.local.json",
|
|
@@ -53671,6 +54635,11 @@ function isTargetEnabled(key, targets) {
|
|
|
53671
54635
|
description: "Analyze instruction quality across all instruction file formats",
|
|
53672
54636
|
default: false
|
|
53673
54637
|
},
|
|
54638
|
+
mcpServers: {
|
|
54639
|
+
type: "boolean",
|
|
54640
|
+
description: "[flag-only, not default-on] Lint MCP server config files (.mcp.json, .vscode/mcp.json)",
|
|
54641
|
+
default: false
|
|
54642
|
+
},
|
|
53674
54643
|
format: {
|
|
53675
54644
|
type: "string",
|
|
53676
54645
|
description: "Output format: human (default), json, or rdjsonl",
|
|
@@ -53681,96 +54650,13 @@ function createLintCommand(lintLessonsCmd) {
|
|
|
53681
54650
|
return defineCommand({
|
|
53682
54651
|
meta: {
|
|
53683
54652
|
name: "lint",
|
|
53684
|
-
description: "Lint agent skills, custom agents, instruction files, and hook configurations. Validates frontmatter, instruction quality, and hook config schemas. Run `lint lessons` to validate lesson files."
|
|
54653
|
+
description: "Lint agent skills, custom agents, instruction files, and hook configurations. Validates frontmatter, instruction quality, and hook config schemas. Also supports flag-only (not default-on) subagent and MCP server config linting. Run `lint lessons` to validate lesson files."
|
|
53685
54654
|
},
|
|
53686
54655
|
subCommands: {
|
|
53687
54656
|
lessons: lintLessonsCmd
|
|
53688
54657
|
},
|
|
53689
54658
|
args: lintArgs,
|
|
53690
|
-
run:
|
|
53691
|
-
const rawTargetDir = typeof context.data?.targetDir === "string" ? context.data.targetDir : process.cwd();
|
|
53692
|
-
const rawArgs_ = context.rawArgs ?? [];
|
|
53693
|
-
const lessonsIsSubcommand = rawArgs_.some((token, idx)=>token === "lessons" && rawArgs_[idx - 1] !== "--format");
|
|
53694
|
-
if (lessonsIsSubcommand) {
|
|
53695
|
-
return;
|
|
53696
|
-
}
|
|
53697
|
-
const lintSkillsFlag = context.args?.skills === true || context.data?.skills === true;
|
|
53698
|
-
const lintAgentsFlag = context.args?.agents === true || context.data?.agents === true;
|
|
53699
|
-
const lintHooksFlag = context.args?.hooks === true || context.data?.hooks === true;
|
|
53700
|
-
const lintInstructionsFlag = context.args?.instructions === true || context.data?.instructions === true;
|
|
53701
|
-
const rawFormat = typeof context.args?.format === "string" ? context.args.format : typeof context.data?.format === "string" ? context.data.format : "human";
|
|
53702
|
-
const validFormats = new Set([
|
|
53703
|
-
"human",
|
|
53704
|
-
"json",
|
|
53705
|
-
"rdjsonl"
|
|
53706
|
-
]);
|
|
53707
|
-
function isLintFormatType(value) {
|
|
53708
|
-
return validFormats.has(value);
|
|
53709
|
-
}
|
|
53710
|
-
const format = isLintFormatType(rawFormat) ? rawFormat : "human";
|
|
53711
|
-
let result;
|
|
53712
|
-
try {
|
|
53713
|
-
result = await runLint({
|
|
53714
|
-
directory: rawTargetDir,
|
|
53715
|
-
targets: {
|
|
53716
|
-
skills: lintSkillsFlag,
|
|
53717
|
-
agents: lintAgentsFlag,
|
|
53718
|
-
hooks: lintHooksFlag,
|
|
53719
|
-
instructions: lintInstructionsFlag
|
|
53720
|
-
}
|
|
53721
|
-
});
|
|
53722
|
-
} catch (error) {
|
|
53723
|
-
if (error instanceof LintValidationError) {
|
|
53724
|
-
consola.error(`Lint failed: ${error.message}`);
|
|
53725
|
-
process.exitCode = 1;
|
|
53726
|
-
return;
|
|
53727
|
-
}
|
|
53728
|
-
throw error;
|
|
53729
|
-
}
|
|
53730
|
-
const { outputs, hasErrors } = result;
|
|
53731
|
-
let totalWarnings = 0;
|
|
53732
|
-
for (const output of outputs){
|
|
53733
|
-
totalWarnings += output.summary.totalWarnings;
|
|
53734
|
-
}
|
|
53735
|
-
const targetLabels = {
|
|
53736
|
-
skill: "skill(s)",
|
|
53737
|
-
agent: "agent(s)",
|
|
53738
|
-
hook: "hook config(s)",
|
|
53739
|
-
instruction: "instruction file(s)"
|
|
53740
|
-
};
|
|
53741
|
-
if (format !== "human") {
|
|
53742
|
-
const formatter = createFormatter(format);
|
|
53743
|
-
const formatted = formatter.format(outputs);
|
|
53744
|
-
if (formatted) {
|
|
53745
|
-
process.stdout.write(`${formatted}\n`);
|
|
53746
|
-
}
|
|
53747
|
-
} else {
|
|
53748
|
-
for (const output of outputs){
|
|
53749
|
-
const label = targetLabels[output.target] ?? output.target;
|
|
53750
|
-
if (output.target === "instruction") {
|
|
53751
|
-
displayInstructionQuality(output);
|
|
53752
|
-
} else {
|
|
53753
|
-
displayLintOutput(output, label);
|
|
53754
|
-
}
|
|
53755
|
-
}
|
|
53756
|
-
}
|
|
53757
|
-
if (hasErrors) {
|
|
53758
|
-
process.exitCode = 1;
|
|
53759
|
-
if (format === "human") {
|
|
53760
|
-
const totalErrors = outputs.reduce((sum, o)=>sum + o.summary.totalErrors, 0);
|
|
53761
|
-
consola.error(`lint failed: ${totalErrors} error(s), ${totalWarnings} warning(s)`);
|
|
53762
|
-
}
|
|
53763
|
-
return;
|
|
53764
|
-
}
|
|
53765
|
-
if (format === "human") {
|
|
53766
|
-
if (totalWarnings > 0) {
|
|
53767
|
-
consola.warn(`Lint passed with ${totalWarnings} warning(s)`);
|
|
53768
|
-
} else {
|
|
53769
|
-
const targets = outputs.map((o)=>targetLabels[o.target] ?? o.target).join(", ");
|
|
53770
|
-
consola.success(`All ${targets} passed lint checks`);
|
|
53771
|
-
}
|
|
53772
|
-
}
|
|
53773
|
-
}
|
|
54659
|
+
run: runLintCommand
|
|
53774
54660
|
});
|
|
53775
54661
|
}
|
|
53776
54662
|
|
|
@@ -63036,4 +63922,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
|
|
|
63036
63922
|
// module factories are used so entry inlining is disabled
|
|
63037
63923
|
// startup
|
|
63038
63924
|
// Load entry module and return exports
|
|
63039
|
-
var __webpack_exports__ = __webpack_require__(
|
|
63925
|
+
var __webpack_exports__ = __webpack_require__(6567);
|