@botbotgo/kit-builtin 0.0.97 → 0.0.98

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.
Files changed (29) hide show
  1. package/dist/core-tools-manifest.json +171 -1
  2. package/dist/src/analyze/analyzeProjectRepo.ast.d.ts +58 -0
  3. package/dist/src/analyze/analyzeProjectRepo.ast.d.ts.map +1 -0
  4. package/dist/src/analyze/analyzeProjectRepo.ast.js +341 -0
  5. package/dist/src/analyze/analyzeProjectRepo.ast.js.map +1 -0
  6. package/dist/src/analyze/analyzeProjectRepo.d.ts +14 -0
  7. package/dist/src/analyze/analyzeProjectRepo.d.ts.map +1 -1
  8. package/dist/src/analyze/analyzeProjectRepo.io.d.ts +47 -0
  9. package/dist/src/analyze/analyzeProjectRepo.io.d.ts.map +1 -0
  10. package/dist/src/analyze/analyzeProjectRepo.io.js +149 -0
  11. package/dist/src/analyze/analyzeProjectRepo.io.js.map +1 -0
  12. package/dist/src/analyze/analyzeProjectRepo.js +66 -266
  13. package/dist/src/analyze/analyzeProjectRepo.js.map +1 -1
  14. package/dist/src/analyze/analyzeProjectRepo.llm.d.ts +15 -0
  15. package/dist/src/analyze/analyzeProjectRepo.llm.d.ts.map +1 -0
  16. package/dist/src/analyze/analyzeProjectRepo.llm.js +122 -0
  17. package/dist/src/analyze/analyzeProjectRepo.llm.js.map +1 -0
  18. package/dist/src/analyze/askProjectRepo.d.ts.map +1 -1
  19. package/dist/src/analyze/askProjectRepo.helpers.d.ts +14 -0
  20. package/dist/src/analyze/askProjectRepo.helpers.d.ts.map +1 -1
  21. package/dist/src/analyze/askProjectRepo.helpers.js +13 -2
  22. package/dist/src/analyze/askProjectRepo.helpers.js.map +1 -1
  23. package/dist/src/analyze/askProjectRepo.js +11 -2
  24. package/dist/src/analyze/askProjectRepo.js.map +1 -1
  25. package/dist/src/fs/searchText.d.ts +1 -1
  26. package/dist/src/fs/searchText.d.ts.map +1 -1
  27. package/package.json +9 -2
  28. package/skills/skill-quality-guard/SKILL.md +72 -0
  29. package/skills/skill-quality-guard/scripts/audit-skill.mjs +427 -0
@@ -0,0 +1,427 @@
1
+ import { readFile, readdir, stat, writeFile } from "node:fs/promises";
2
+ import { resolve, join, relative } from "node:path";
3
+ import { parseMarkdownYamlFrontmatter } from "@botbotgo/common/utils";
4
+
5
+ const NAME_PATTERN = /^[a-z0-9-]+$/;
6
+ const RESERVED_WORDS = ["anthropic", "claude"];
7
+ const REQUIRED_SECTIONS = ["goal", "inputs", "workflow"];
8
+ const DISCOURAGED_FILES = [".DS_Store", "Thumbs.db"];
9
+ const DIRTY_DIRS = ["node_modules", "dist", "build", "output", ".cache", ".git"];
10
+
11
+ function parseArgs(argv) {
12
+ const out = {};
13
+ for (let i = 0; i < argv.length; i += 1) {
14
+ const token = argv[i];
15
+ if (!token.startsWith("--")) continue;
16
+ const key = token.slice(2);
17
+ const next = argv[i + 1];
18
+ if (!next || next.startsWith("--")) {
19
+ out[key] = "true";
20
+ continue;
21
+ }
22
+ out[key] = next;
23
+ i += 1;
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function toStringValue(v) {
29
+ if (typeof v === "string") return v;
30
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
31
+ if (Array.isArray(v)) return v.map((x) => toStringValue(x)).join("\n");
32
+ return "";
33
+ }
34
+
35
+ function addFinding(findings, severity, id, message, suggestion, path) {
36
+ findings.push({ severity, id, message, suggestion, ...(path ? { path } : {}) });
37
+ }
38
+
39
+ function validateFrontmatter(frontmatter, findings, filePath) {
40
+ const name = toStringValue(frontmatter.name).trim();
41
+ const description = toStringValue(frontmatter.description).trim();
42
+ const compatibility = toStringValue(frontmatter.compatibility).trim();
43
+ const allowedTools = toStringValue(frontmatter["allowed-tools"] || frontmatter.allowedTools).trim();
44
+
45
+ if (!name) {
46
+ addFinding(findings, "error", "frontmatter-name-missing", "Missing required frontmatter field: name", "Add a lowercase hyphenated name in YAML frontmatter.", filePath);
47
+ } else {
48
+ if (name.length > 64) {
49
+ addFinding(findings, "error", "frontmatter-name-too-long", `Frontmatter name exceeds 64 chars (${name.length})`, "Shorten the skill name to <=64 characters.", filePath);
50
+ }
51
+ if (!NAME_PATTERN.test(name) || name.startsWith("-") || name.endsWith("-") || name.includes("--")) {
52
+ addFinding(findings, "error", "frontmatter-name-invalid", "Frontmatter name must use lowercase letters, numbers, and single hyphens", "Use a name like `skill-quality-guard`.", filePath);
53
+ }
54
+ for (const reserved of RESERVED_WORDS) {
55
+ if (name.includes(reserved)) {
56
+ addFinding(findings, "error", "frontmatter-name-reserved", `Frontmatter name cannot include reserved word: ${reserved}`, "Choose a neutral tool name.", filePath);
57
+ }
58
+ }
59
+ }
60
+
61
+ if (!description) {
62
+ addFinding(findings, "error", "frontmatter-description-missing", "Missing required frontmatter field: description", "Add a concise description in frontmatter.", filePath);
63
+ } else if (description.length > 1024) {
64
+ addFinding(findings, "error", "frontmatter-description-too-long", `Frontmatter description exceeds 1024 chars (${description.length})`, "Shorten description to <=1024 characters.", filePath);
65
+ }
66
+
67
+ if (compatibility.length > 500) {
68
+ addFinding(findings, "warning", "frontmatter-compatibility-too-long", `Frontmatter compatibility is very long (${compatibility.length})`, "Keep compatibility short to improve readability.", filePath);
69
+ }
70
+
71
+ if (allowedTools) {
72
+ const tools = allowedTools
73
+ .split(/[\s,]+/)
74
+ .map((x) => x.trim())
75
+ .filter(Boolean);
76
+ if (tools.includes("*")) {
77
+ addFinding(findings, "warning", "allowed-tools-wildcard", "allowed-tools uses wildcard (*)", "Prefer explicit tool allowlist for least privilege.", filePath);
78
+ }
79
+ }
80
+ }
81
+
82
+ function validateSections(instructions, findings, filePath) {
83
+ const headings = new Set(
84
+ String(instructions || "")
85
+ .split("\n")
86
+ .map((line) => line.trim())
87
+ .filter((line) => line.startsWith("#"))
88
+ .map((line) => line.replace(/^#+\s*/, "").toLowerCase()),
89
+ );
90
+
91
+ for (const section of REQUIRED_SECTIONS) {
92
+ if (!headings.has(section)) {
93
+ addFinding(findings, "warning", `section-${section}-missing`, `Suggested section missing: ${section}`, `Add a \`${section[0].toUpperCase()}${section.slice(1)}\` section for consistency.`, filePath);
94
+ }
95
+ }
96
+
97
+ const text = String(instructions || "");
98
+ if (text.length > 12000) {
99
+ addFinding(findings, "warning", "instructions-too-long", `Instruction body is very long (${text.length} chars)`, "Move long details into references/*.md and keep SKILL.md concise.", filePath);
100
+ }
101
+ if (/\b(TODO|TBD|FIXME)\b/i.test(text)) {
102
+ addFinding(findings, "warning", "instructions-placeholder", "Instruction body contains TODO/TBD/FIXME placeholders", "Replace placeholders with executable steps.", filePath);
103
+ }
104
+ if (/\/(Users|home)\/[\w.-]+\//.test(text) || /[A-Za-z]:\\\\/.test(text)) {
105
+ addFinding(findings, "warning", "instructions-absolute-path", "Instruction body contains absolute local paths", "Use relative paths so the skill is portable.", filePath);
106
+ }
107
+ }
108
+
109
+ function validateStepPrinciples(instructions, findings, filePath) {
110
+ const text = String(instructions || "");
111
+ const stepRegex = /^##+\s+Step[^\n]*$/gim;
112
+ const matches = [...text.matchAll(stepRegex)];
113
+
114
+ if (matches.length === 0) {
115
+ addFinding(
116
+ findings,
117
+ "warning",
118
+ "workflow-step-structure-missing",
119
+ "Workflow does not define explicit Step sections",
120
+ "Use explicit Step sections so each step can be audited by execution style.",
121
+ filePath,
122
+ );
123
+ return;
124
+ }
125
+
126
+ for (let i = 0; i < matches.length; i += 1) {
127
+ const current = matches[i];
128
+ const next = matches[i + 1];
129
+ const title = (current[0] || "").trim();
130
+ const start = current.index + current[0].length;
131
+ const end = next ? next.index : text.length;
132
+ const block = text.slice(start, end);
133
+
134
+ const hasCommands =
135
+ /```(?:bash|sh|shell)[\s\S]*?```/i.test(block) ||
136
+ /(?:^|\n)\s*\d+\.\s*Commands?\b/i.test(block) ||
137
+ /(?:^|\n)\s*[-*]\s*`[^`\n]+`/.test(block);
138
+ const hasTools =
139
+ /(?:^|\n)\s*\d+\.\s*Tools?\b/i.test(block) ||
140
+ /(?:^|\n)\s*[-*]\s*`(?:execute|read_file|write_file|list_dir|searchText|analyzeProjectRepo|askProjectRepo)`/i.test(block);
141
+ const hasRequirement = /(?:^|\n)\s*\d+\.\s*Requirement\b/i.test(block);
142
+ const hasValidation = /(?:^|\n)\s*\d+\.\s*Validation(?:\s+and\s+Retry)?\b/i.test(block);
143
+ const hasRetry = /\bretry\b/i.test(block);
144
+ const writesResource = /\b(write|save|create|render|generate)\b/i.test(block)
145
+ && /\b(file|json|html|artifact|report|output)\b/i.test(block);
146
+ const hasExplicitLocation =
147
+ /(?:^|\n)\s*\d+\.\s*Data\b[\s\S]*?(?:\bpath\b|\blocation\b|\boutputPath\b)/i.test(block) ||
148
+ /(?:^|\n)\s*\d+\.\s*Result\b[\s\S]*?`[^`\n/]+\/[^`\n]+`/i.test(block) ||
149
+ /(?:^|\n)\s*\d+\.\s*Commands?\b[\s\S]*?(?:--out\s+|--output\s+|output\/|`[^`\n/]+\/[^`\n]+`)/i.test(block) ||
150
+ /`[^`\n/]+\/[^`\n]+`/.test(block);
151
+
152
+ const modeCount = [hasCommands, hasTools, hasRequirement].filter(Boolean).length;
153
+ if (modeCount === 0) {
154
+ addFinding(
155
+ findings,
156
+ "warning",
157
+ "step-principle-missing",
158
+ `Step does not match any execution principle: ${title}`,
159
+ "Each step should be clearly Command-first, Tool-first, or Requirement-first.",
160
+ filePath,
161
+ );
162
+ }
163
+
164
+ const complexHint = /\b(analyze|extract|generate|build|render|workflow|diagram|report)\b/i.test(block);
165
+ if (complexHint && (!hasValidation || !hasRetry)) {
166
+ addFinding(
167
+ findings,
168
+ "warning",
169
+ "complex-step-validation-retry-missing",
170
+ `Complex step should define validation and retry: ${title}`,
171
+ "Add explicit Validation and Retry rules for complex steps.",
172
+ filePath,
173
+ );
174
+ }
175
+
176
+ if (writesResource && !hasExplicitLocation) {
177
+ addFinding(
178
+ findings,
179
+ "warning",
180
+ "generated-resource-location-missing",
181
+ `Generated resource step must define explicit output location: ${title}`,
182
+ "Add a concrete output path (for example `output/report.json`) in Data/Result/Commands.",
183
+ filePath,
184
+ );
185
+ }
186
+ }
187
+ }
188
+
189
+ function extractSectionBody(markdown, headingName) {
190
+ const text = String(markdown || "");
191
+ const lines = text.split("\n");
192
+ const startIdx = lines.findIndex((line) => /^##\s+/.test(line.trim())
193
+ && line.trim().replace(/^##\s+/, "").toLowerCase() === headingName.toLowerCase());
194
+ if (startIdx < 0) return "";
195
+ let endIdx = lines.length;
196
+ for (let i = startIdx + 1; i < lines.length; i += 1) {
197
+ if (/^##\s+/.test(lines[i].trim())) {
198
+ endIdx = i;
199
+ break;
200
+ }
201
+ }
202
+ return lines.slice(startIdx + 1, endIdx).join("\n");
203
+ }
204
+
205
+ function validateWorkflowOnlySteps(instructions, findings, filePath) {
206
+ const workflowBody = extractSectionBody(instructions, "Workflow");
207
+ if (!workflowBody.trim()) return;
208
+
209
+ const blocks = workflowBody
210
+ .split(/\n(?=##+\s+Step\b)/i)
211
+ .map((x) => x.trim())
212
+ .filter(Boolean);
213
+
214
+ if (blocks.length === 0) {
215
+ addFinding(
216
+ findings,
217
+ "warning",
218
+ "workflow-only-steps-missing",
219
+ "Workflow section should contain explicit Step blocks only",
220
+ "Use `## Step N: ...` blocks under Workflow and remove unrelated narrative text.",
221
+ filePath,
222
+ );
223
+ return;
224
+ }
225
+
226
+ const beforeFirstStep = workflowBody.split(/##+\s+Step\b/i)[0] || "";
227
+ const noiseLines = beforeFirstStep
228
+ .split("\n")
229
+ .map((line) => line.trim())
230
+ .filter((line) => line.length > 0)
231
+ .filter((line) => !line.startsWith("```"))
232
+ .filter((line) => !/^[-*]\s+/.test(line))
233
+ .filter((line) => !/^\d+\.\s+/.test(line));
234
+
235
+ if (noiseLines.length > 0) {
236
+ addFinding(
237
+ findings,
238
+ "warning",
239
+ "workflow-nonstep-content",
240
+ "Workflow section contains non-step content",
241
+ "Keep Workflow focused on concrete steps only; move other notes to another section.",
242
+ filePath,
243
+ );
244
+ }
245
+ }
246
+
247
+ async function walk(rootDir, currentDir, result) {
248
+ const entries = await readdir(currentDir, { withFileTypes: true });
249
+ for (const entry of entries) {
250
+ const abs = join(currentDir, entry.name);
251
+ const rel = relative(rootDir, abs).replaceAll("\\", "/");
252
+ if (entry.isDirectory()) {
253
+ result.directories.push(rel || ".");
254
+ if (DIRTY_DIRS.includes(entry.name)) {
255
+ continue;
256
+ }
257
+ await walk(rootDir, abs, result);
258
+ continue;
259
+ }
260
+ if (entry.isFile()) {
261
+ const st = await stat(abs);
262
+ result.files.push({ relPath: rel, size: st.size });
263
+ }
264
+ }
265
+ }
266
+
267
+ function validateCleanliness(walkResult, findings) {
268
+ const filesByDir = new Map();
269
+ for (const file of walkResult.files) {
270
+ const idx = file.relPath.lastIndexOf("/");
271
+ const dir = idx >= 0 ? file.relPath.slice(0, idx) : ".";
272
+ filesByDir.set(dir, (filesByDir.get(dir) || 0) + 1);
273
+
274
+ const base = file.relPath.split("/").pop() || file.relPath;
275
+ if (DISCOURAGED_FILES.includes(base)) {
276
+ addFinding(findings, "warning", "dirty-os-artifact", `Unexpected OS artifact file: ${file.relPath}`, "Remove editor/OS generated files from skill directories.", file.relPath);
277
+ }
278
+
279
+ if (file.relPath.endsWith(".log") || file.relPath.endsWith(".tmp")) {
280
+ addFinding(findings, "warning", "dirty-temp-file", `Unexpected temporary/log file: ${file.relPath}`, "Remove generated artifacts from skill folders.", file.relPath);
281
+ }
282
+
283
+ if (file.size > 256 * 1024) {
284
+ addFinding(findings, "warning", "oversized-file", `Large skill asset file: ${file.relPath} (${file.size} bytes)`, "Store heavy data outside skill assets or trim content.", file.relPath);
285
+ }
286
+ }
287
+
288
+ for (const dir of walkResult.directories) {
289
+ const leaf = dir.split("/").pop() || dir;
290
+ if (DIRTY_DIRS.includes(leaf)) {
291
+ addFinding(findings, "warning", "dirty-build-dir", `Generated/build directory detected inside skill: ${dir}`, "Do not keep build/cache/output folders inside skills.", dir);
292
+ }
293
+ }
294
+
295
+ for (const [dir, fileCount] of filesByDir.entries()) {
296
+ if (fileCount > 10) {
297
+ addFinding(findings, "info", "folder-density-high", `Folder has more than 10 files (${fileCount}): ${dir}`, "Consider splitting files into subfolders for maintainability.", dir);
298
+ }
299
+ }
300
+ }
301
+
302
+ function validateReferences(instructions, walkResult, findings) {
303
+ const refs = new Set();
304
+ const text = String(instructions || "");
305
+ const patterns = [
306
+ /(?:^|\s)(scripts\/[A-Za-z0-9._\/-]+)/gm,
307
+ /(?:^|\s)(references\/[A-Za-z0-9._\/-]+)/gm,
308
+ /(?:^|\s)(assets\/[A-Za-z0-9._\/-]+)/gm,
309
+ ];
310
+
311
+ for (const pattern of patterns) {
312
+ let m = pattern.exec(text);
313
+ while (m) {
314
+ refs.add(m[1].trim());
315
+ m = pattern.exec(text);
316
+ }
317
+ }
318
+
319
+ const fileSet = new Set(walkResult.files.map((f) => f.relPath));
320
+ for (const refPath of refs) {
321
+ if (!fileSet.has(refPath)) {
322
+ addFinding(findings, "error", "missing-referenced-asset", `Referenced asset not found: ${refPath}`, "Create the file or remove invalid reference in SKILL.md.", refPath);
323
+ }
324
+ }
325
+ }
326
+
327
+ function summarize(findings) {
328
+ const counts = { error: 0, warning: 0, info: 0 };
329
+ for (const f of findings) counts[f.severity] += 1;
330
+ const score = Math.max(0, 100 - counts.error * 20 - counts.warning * 6 - counts.info * 2);
331
+ const ok = counts.error === 0;
332
+ return { ok, score, counts };
333
+ }
334
+
335
+ export async function auditSkill({ skillPath, strict = false, outputPath } = {}) {
336
+ if (!skillPath || typeof skillPath !== "string") {
337
+ throw new Error("skillPath is required");
338
+ }
339
+
340
+ const root = resolve(skillPath);
341
+ const skillMdPath = join(root, "SKILL.md");
342
+ const findings = [];
343
+
344
+ let skillMd = "";
345
+ try {
346
+ skillMd = await readFile(skillMdPath, "utf8");
347
+ } catch {
348
+ addFinding(findings, "error", "skill-md-missing", "SKILL.md not found", "Create SKILL.md with YAML frontmatter and workflow instructions.", "SKILL.md");
349
+ const summary = summarize(findings);
350
+ const result = {
351
+ skillPath: root,
352
+ checkedAt: new Date().toISOString(),
353
+ ...summary,
354
+ findings,
355
+ };
356
+ if (outputPath) {
357
+ await writeFile(resolve(outputPath), `${JSON.stringify(result, null, 2)}\n`, "utf8");
358
+ }
359
+ if (strict) {
360
+ throw new Error(`Skill audit failed with ${summary.counts.error} error(s)`);
361
+ }
362
+ return result;
363
+ }
364
+
365
+ let frontmatter = {};
366
+ let instructions = "";
367
+ try {
368
+ const parsed = parseMarkdownYamlFrontmatter(skillMd, skillMdPath);
369
+ frontmatter = parsed.frontmatter || {};
370
+ instructions = parsed.body || "";
371
+ } catch (error) {
372
+ addFinding(findings, "error", "frontmatter-parse-failed", `Failed to parse YAML frontmatter: ${error instanceof Error ? error.message : String(error)}`, "Fix YAML frontmatter format in SKILL.md.", "SKILL.md");
373
+ }
374
+
375
+ validateFrontmatter(frontmatter, findings, "SKILL.md");
376
+ validateSections(instructions, findings, "SKILL.md");
377
+ validateWorkflowOnlySteps(instructions, findings, "SKILL.md");
378
+ validateStepPrinciples(instructions, findings, "SKILL.md");
379
+
380
+ const walkResult = { files: [], directories: [] };
381
+ await walk(root, root, walkResult);
382
+ validateCleanliness(walkResult, findings);
383
+ validateReferences(instructions, walkResult, findings);
384
+
385
+ const summary = summarize(findings);
386
+ const result = {
387
+ skillPath: root,
388
+ checkedAt: new Date().toISOString(),
389
+ ...summary,
390
+ findings,
391
+ stats: {
392
+ fileCount: walkResult.files.length,
393
+ dirCount: walkResult.directories.length,
394
+ },
395
+ };
396
+
397
+ if (outputPath) {
398
+ await writeFile(resolve(outputPath), `${JSON.stringify(result, null, 2)}\n`, "utf8");
399
+ }
400
+
401
+ if (strict && !summary.ok) {
402
+ throw new Error(`Skill audit failed: ${summary.counts.error} error(s), ${summary.counts.warning} warning(s)`);
403
+ }
404
+
405
+ return result;
406
+ }
407
+
408
+ async function main() {
409
+ const args = parseArgs(process.argv.slice(2));
410
+ const skillPath = typeof args.skill === "string" ? args.skill : "";
411
+ const strict = args.strict === "true";
412
+ const outputPath = typeof args.out === "string" && args.out.trim() ? args.out.trim() : undefined;
413
+
414
+ const report = await auditSkill({ skillPath, strict, outputPath });
415
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
416
+
417
+ if (strict && !report.ok) {
418
+ process.exitCode = 1;
419
+ }
420
+ }
421
+
422
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
423
+ main().catch((error) => {
424
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
425
+ process.exit(1);
426
+ });
427
+ }