claude-code-templates 1.22.0 → 1.22.2
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/README.md +56 -0
- package/bin/create-claude-config.js +1 -0
- package/package.json +7 -2
- package/src/analytics-web/chats_mobile.html +17 -16
- package/src/console-bridge.js +3 -3
- package/src/index.js +183 -9
- package/src/security-audit.js +164 -0
- package/src/validation/ARCHITECTURE.md +309 -0
- package/src/validation/BaseValidator.js +152 -0
- package/src/validation/README.md +543 -0
- package/src/validation/ValidationOrchestrator.js +305 -0
- package/src/validation/validators/IntegrityValidator.js +338 -0
- package/src/validation/validators/ProvenanceValidator.js +399 -0
- package/src/validation/validators/ReferenceValidator.js +373 -0
- package/src/validation/validators/SemanticValidator.js +449 -0
- package/src/validation/validators/StructuralValidator.js +376 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
const BaseValidator = require('../BaseValidator');
|
|
2
|
+
const yaml = require('js-yaml');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* StructuralValidator - Validates component structure and format
|
|
6
|
+
*
|
|
7
|
+
* Checks:
|
|
8
|
+
* - YAML frontmatter presence and validity
|
|
9
|
+
* - Required fields (name, description)
|
|
10
|
+
* - File size limits
|
|
11
|
+
* - UTF-8 encoding
|
|
12
|
+
* - Section count limits
|
|
13
|
+
* - Component type-specific requirements
|
|
14
|
+
*/
|
|
15
|
+
class StructuralValidator extends BaseValidator {
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
|
|
19
|
+
// Configuration limits
|
|
20
|
+
this.MAX_FILE_SIZE = 100 * 1024; // 100KB
|
|
21
|
+
this.MAX_SECTION_COUNT = 20; // Prevent context overflow
|
|
22
|
+
this.MIN_DESCRIPTION_LENGTH = 20;
|
|
23
|
+
this.MAX_DESCRIPTION_LENGTH = 500;
|
|
24
|
+
|
|
25
|
+
// Required fields by component type
|
|
26
|
+
this.REQUIRED_FIELDS = {
|
|
27
|
+
agent: ['name', 'description', 'tools'],
|
|
28
|
+
command: ['name', 'description'],
|
|
29
|
+
mcp: ['name', 'description', 'command'],
|
|
30
|
+
setting: ['name', 'description'],
|
|
31
|
+
hook: ['name', 'description', 'trigger']
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Optional but recommended fields
|
|
35
|
+
this.RECOMMENDED_FIELDS = {
|
|
36
|
+
agent: ['model'],
|
|
37
|
+
command: ['usage', 'examples'],
|
|
38
|
+
mcp: ['args'],
|
|
39
|
+
setting: ['type'],
|
|
40
|
+
hook: ['conditions']
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate component structure
|
|
46
|
+
* @param {object} component - Component data
|
|
47
|
+
* @param {string} component.content - Raw markdown content
|
|
48
|
+
* @param {string} component.path - File path
|
|
49
|
+
* @param {string} component.type - Component type (agent, command, mcp, etc.)
|
|
50
|
+
* @param {object} options - Validation options
|
|
51
|
+
* @returns {Promise<object>} Validation results
|
|
52
|
+
*/
|
|
53
|
+
async validate(component, options = {}) {
|
|
54
|
+
this.reset();
|
|
55
|
+
|
|
56
|
+
const { content, path, type } = component;
|
|
57
|
+
|
|
58
|
+
if (!content) {
|
|
59
|
+
this.addError('STRUCT_E001', 'Component content is empty or missing', { path });
|
|
60
|
+
return this.getResults();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 1. File size validation
|
|
64
|
+
this.validateFileSize(content, path);
|
|
65
|
+
|
|
66
|
+
// 2. UTF-8 encoding validation
|
|
67
|
+
this.validateEncoding(content, path);
|
|
68
|
+
|
|
69
|
+
// 3. Frontmatter validation
|
|
70
|
+
const frontmatter = this.validateFrontmatter(content, path);
|
|
71
|
+
|
|
72
|
+
if (frontmatter) {
|
|
73
|
+
// 4. Required fields validation
|
|
74
|
+
this.validateRequiredFields(frontmatter, type, path);
|
|
75
|
+
|
|
76
|
+
// 5. Description validation
|
|
77
|
+
this.validateDescription(frontmatter, path);
|
|
78
|
+
|
|
79
|
+
// 6. Tools validation (for agents)
|
|
80
|
+
if (type === 'agent') {
|
|
81
|
+
this.validateTools(frontmatter, path);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 7. Model validation (for agents)
|
|
85
|
+
if (type === 'agent') {
|
|
86
|
+
this.validateModel(frontmatter, path);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 8. Recommended fields check
|
|
90
|
+
this.checkRecommendedFields(frontmatter, type, path);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 9. Content structure validation
|
|
94
|
+
this.validateContentStructure(content, path);
|
|
95
|
+
|
|
96
|
+
// 10. Section count validation
|
|
97
|
+
this.validateSectionCount(content, path);
|
|
98
|
+
|
|
99
|
+
return this.getResults();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate file size
|
|
104
|
+
*/
|
|
105
|
+
validateFileSize(content, path) {
|
|
106
|
+
const size = Buffer.byteLength(content, 'utf8');
|
|
107
|
+
|
|
108
|
+
if (size > this.MAX_FILE_SIZE) {
|
|
109
|
+
this.addError(
|
|
110
|
+
'STRUCT_E003',
|
|
111
|
+
`File size (${(size / 1024).toFixed(2)}KB) exceeds maximum allowed size (${this.MAX_FILE_SIZE / 1024}KB)`,
|
|
112
|
+
{ path, size, limit: this.MAX_FILE_SIZE }
|
|
113
|
+
);
|
|
114
|
+
} else if (size > this.MAX_FILE_SIZE * 0.8) {
|
|
115
|
+
this.addWarning(
|
|
116
|
+
'STRUCT_W002',
|
|
117
|
+
`File size (${(size / 1024).toFixed(2)}KB) is approaching the limit`,
|
|
118
|
+
{ path, size, limit: this.MAX_FILE_SIZE }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.addInfo('STRUCT_I001', `File size: ${(size / 1024).toFixed(2)}KB`, { path, size });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Validate UTF-8 encoding
|
|
127
|
+
*/
|
|
128
|
+
validateEncoding(content, path) {
|
|
129
|
+
try {
|
|
130
|
+
// Try to detect non-UTF-8 characters
|
|
131
|
+
const buffer = Buffer.from(content, 'utf8');
|
|
132
|
+
const decoded = buffer.toString('utf8');
|
|
133
|
+
|
|
134
|
+
if (decoded !== content) {
|
|
135
|
+
this.addError(
|
|
136
|
+
'STRUCT_E004',
|
|
137
|
+
'File contains invalid UTF-8 encoding',
|
|
138
|
+
{ path }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for null bytes (potential binary content)
|
|
143
|
+
if (content.includes('\0')) {
|
|
144
|
+
this.addError(
|
|
145
|
+
'STRUCT_E005',
|
|
146
|
+
'File contains null bytes (possible binary content)',
|
|
147
|
+
{ path }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this.addError(
|
|
152
|
+
'STRUCT_E004',
|
|
153
|
+
'Failed to validate encoding',
|
|
154
|
+
{ path, error: error.message }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate and parse YAML frontmatter
|
|
161
|
+
* @returns {object|null} Parsed frontmatter or null if invalid
|
|
162
|
+
*/
|
|
163
|
+
validateFrontmatter(content, path) {
|
|
164
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
165
|
+
|
|
166
|
+
if (!frontmatterMatch) {
|
|
167
|
+
this.addError(
|
|
168
|
+
'STRUCT_E001',
|
|
169
|
+
'Missing YAML frontmatter (must start with --- and end with ---)',
|
|
170
|
+
{ path }
|
|
171
|
+
);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const frontmatter = yaml.load(frontmatterMatch[1]);
|
|
177
|
+
|
|
178
|
+
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
179
|
+
this.addError(
|
|
180
|
+
'STRUCT_E002',
|
|
181
|
+
'Frontmatter is empty or not a valid object',
|
|
182
|
+
{ path }
|
|
183
|
+
);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.addInfo('STRUCT_I002', 'Valid YAML frontmatter found', { path });
|
|
188
|
+
return frontmatter;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.addError(
|
|
191
|
+
'STRUCT_E002',
|
|
192
|
+
`Invalid YAML syntax in frontmatter: ${error.message}`,
|
|
193
|
+
{ path, error: error.message }
|
|
194
|
+
);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validate required fields
|
|
201
|
+
*/
|
|
202
|
+
validateRequiredFields(frontmatter, type, path) {
|
|
203
|
+
const requiredFields = this.REQUIRED_FIELDS[type] || ['name', 'description'];
|
|
204
|
+
|
|
205
|
+
for (const field of requiredFields) {
|
|
206
|
+
if (!frontmatter[field]) {
|
|
207
|
+
this.addError(
|
|
208
|
+
'STRUCT_E006',
|
|
209
|
+
`Missing required field: ${field}`,
|
|
210
|
+
{ path, field, type }
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Validate description field
|
|
218
|
+
*/
|
|
219
|
+
validateDescription(frontmatter, path) {
|
|
220
|
+
const description = frontmatter.description;
|
|
221
|
+
|
|
222
|
+
if (!description) return; // Already caught by required fields
|
|
223
|
+
|
|
224
|
+
if (typeof description !== 'string') {
|
|
225
|
+
this.addError(
|
|
226
|
+
'STRUCT_E007',
|
|
227
|
+
'Description must be a string',
|
|
228
|
+
{ path, type: typeof description }
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const length = description.trim().length;
|
|
234
|
+
|
|
235
|
+
if (length < this.MIN_DESCRIPTION_LENGTH) {
|
|
236
|
+
this.addWarning(
|
|
237
|
+
'STRUCT_W003',
|
|
238
|
+
`Description is too short (${length} chars, minimum ${this.MIN_DESCRIPTION_LENGTH})`,
|
|
239
|
+
{ path, length, min: this.MIN_DESCRIPTION_LENGTH }
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (length > this.MAX_DESCRIPTION_LENGTH) {
|
|
244
|
+
this.addWarning(
|
|
245
|
+
'STRUCT_W004',
|
|
246
|
+
`Description is too long (${length} chars, maximum ${this.MAX_DESCRIPTION_LENGTH})`,
|
|
247
|
+
{ path, length, max: this.MAX_DESCRIPTION_LENGTH }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Validate tools field for agents
|
|
254
|
+
*/
|
|
255
|
+
validateTools(frontmatter, path) {
|
|
256
|
+
const tools = frontmatter.tools;
|
|
257
|
+
|
|
258
|
+
if (!tools) return; // Already caught by required fields
|
|
259
|
+
|
|
260
|
+
// Tools can be a string (comma-separated) or array
|
|
261
|
+
if (typeof tools === 'string') {
|
|
262
|
+
const toolsList = tools.split(',').map(t => t.trim()).filter(t => t);
|
|
263
|
+
|
|
264
|
+
if (toolsList.length === 0) {
|
|
265
|
+
this.addWarning('STRUCT_W005', 'Tools field is empty', { path });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Validate known tool names
|
|
269
|
+
const validTools = ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'WebSearch', 'WebFetch', '*'];
|
|
270
|
+
const invalidTools = toolsList.filter(t => !validTools.includes(t) && t !== '*');
|
|
271
|
+
|
|
272
|
+
if (invalidTools.length > 0) {
|
|
273
|
+
this.addWarning(
|
|
274
|
+
'STRUCT_W006',
|
|
275
|
+
`Unknown tools specified: ${invalidTools.join(', ')}`,
|
|
276
|
+
{ path, invalidTools }
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
} else if (Array.isArray(tools)) {
|
|
280
|
+
if (tools.length === 0) {
|
|
281
|
+
this.addWarning('STRUCT_W005', 'Tools array is empty', { path });
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
this.addError(
|
|
285
|
+
'STRUCT_E008',
|
|
286
|
+
'Tools field must be a string or array',
|
|
287
|
+
{ path, type: typeof tools }
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Validate model field for agents
|
|
294
|
+
*/
|
|
295
|
+
validateModel(frontmatter, path) {
|
|
296
|
+
const model = frontmatter.model;
|
|
297
|
+
|
|
298
|
+
if (!model) {
|
|
299
|
+
this.addWarning('STRUCT_W007', 'No model specified (recommended)', { path });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const validModels = ['sonnet', 'opus', 'haiku', 'claude-3-5-sonnet', 'claude-3-opus', 'claude-3-haiku'];
|
|
304
|
+
|
|
305
|
+
if (!validModels.includes(model)) {
|
|
306
|
+
this.addWarning(
|
|
307
|
+
'STRUCT_W008',
|
|
308
|
+
`Unknown model: ${model}. Valid models: ${validModels.join(', ')}`,
|
|
309
|
+
{ path, model }
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check for recommended fields
|
|
316
|
+
*/
|
|
317
|
+
checkRecommendedFields(frontmatter, type, path) {
|
|
318
|
+
const recommendedFields = this.RECOMMENDED_FIELDS[type] || [];
|
|
319
|
+
const missingFields = recommendedFields.filter(field => !frontmatter[field]);
|
|
320
|
+
|
|
321
|
+
if (missingFields.length > 0) {
|
|
322
|
+
this.addInfo(
|
|
323
|
+
'STRUCT_I003',
|
|
324
|
+
`Missing recommended fields: ${missingFields.join(', ')}`,
|
|
325
|
+
{ path, missingFields }
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Validate content structure
|
|
332
|
+
*/
|
|
333
|
+
validateContentStructure(content, path) {
|
|
334
|
+
// Remove frontmatter for content analysis
|
|
335
|
+
const contentWithoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n/, '');
|
|
336
|
+
|
|
337
|
+
if (contentWithoutFrontmatter.trim().length < 50) {
|
|
338
|
+
this.addWarning(
|
|
339
|
+
'STRUCT_W009',
|
|
340
|
+
'Component content is very short (less than 50 characters)',
|
|
341
|
+
{ path, length: contentWithoutFrontmatter.trim().length }
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Check for basic markdown structure
|
|
346
|
+
const hasHeaders = /^#{1,6}\s+.+$/m.test(contentWithoutFrontmatter);
|
|
347
|
+
|
|
348
|
+
if (!hasHeaders) {
|
|
349
|
+
this.addWarning(
|
|
350
|
+
'STRUCT_W010',
|
|
351
|
+
'No markdown headers found in content (recommended for organization)',
|
|
352
|
+
{ path }
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Validate section count (prevent context overflow)
|
|
359
|
+
*/
|
|
360
|
+
validateSectionCount(content, path) {
|
|
361
|
+
const sections = content.match(/^#{1,6}\s+.+$/gm) || [];
|
|
362
|
+
const count = sections.length;
|
|
363
|
+
|
|
364
|
+
if (count > this.MAX_SECTION_COUNT) {
|
|
365
|
+
this.addWarning(
|
|
366
|
+
'STRUCT_W011',
|
|
367
|
+
`Too many sections (${count}), may cause context overflow. Maximum recommended: ${this.MAX_SECTION_COUNT}`,
|
|
368
|
+
{ path, count, max: this.MAX_SECTION_COUNT }
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.addInfo('STRUCT_I004', `Section count: ${count}`, { path, count });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
module.exports = StructuralValidator;
|