diff --git a/skills/rig/samples/371-npm-lifecycle-script-analyzer.md b/skills/rig/samples/371-npm-lifecycle-script-analyzer.md new file mode 100644 index 0000000..456ace6 --- /dev/null +++ b/skills/rig/samples/371-npm-lifecycle-script-analyzer.md @@ -0,0 +1,54 @@ +# 371 - NPM Lifecycle Script Analyzer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const classifyScript = defineTool("classifyScript", { + description: "Classify an npm script by name and command into a lifecycle category.", + parameters: s.object({ scriptName: s.string, command: s.string }), + handler({ scriptName }) { + const name = scriptName.toLowerCase(); + const isHook = /^(pre|post)/.test(name); + if (/build|compile|bundle|webpack|rollup|esbuild|tsc/.test(name)) return { category: "build" as const, isHook }; + if (/test|jest|vitest|mocha|spec|coverage/.test(name)) return { category: "test" as const, isHook }; + if (/lint|eslint|tslint|prettier|format|check/.test(name)) return { category: "lint" as const, isHook }; + if (/release|publish|deploy|version|changelog/.test(name)) return { category: "release" as const, isHook }; + if (isHook) return { category: "hook" as const, isHook: true }; + return { category: "other" as const, isHook: false }; + }, +}); + +// Agent role: analyze npm lifecycle scripts in package.json and classify each one. +const npmLifecycleScriptAnalyzer = agent({ + model: "small", + instructions: p`Analyze the npm lifecycle scripts defined in package.json. + +package.json contents: +${p.read("package.json")} + +For each entry in the "scripts" field, call classifyScript with the script name and command. +Build a scripts record keyed by script name with command, category, and isHook fields. +Count hookCount (total scripts where isHook is true). +List missingRecommended: which of ["test", "build", "lint"] category names are absent from the scripts. +Set hasTestScript to true if any script has category "test", hasBuildScript if any has category "build".`, + tools: [classifyScript], + output: s.object({ + scripts: s.record( + s.object({ + command: s.string, + category: s.enum("build", "test", "lint", "release", "hook", "other"), + isHook: s.boolean, + }) + ), + hookCount: s.int, + missingRecommended: s.array(s.string), + hasTestScript: s.boolean, + hasBuildScript: s.boolean, + }), + maxTurns: 4, + addons: repair(), +}); + +export default npmLifecycleScriptAnalyzer; + +``` diff --git a/skills/rig/samples/372-ts-jsdoc-coverage-checker.md b/skills/rig/samples/372-ts-jsdoc-coverage-checker.md new file mode 100644 index 0000000..ed3ae2d --- /dev/null +++ b/skills/rig/samples/372-ts-jsdoc-coverage-checker.md @@ -0,0 +1,64 @@ +# 372 - TS JSDoc Coverage Checker + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const analyzeFunctionComments = defineTool("analyzeFunctionComments", { + description: "Count exported functions with and without JSDoc in a TypeScript file.", + parameters: s.object({ filePath: s.string }), + async handler({ filePath }) { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(filePath, "utf8"); + const lines = content.split("\n"); + let documentedCount = 0; + let undocumentedCount = 0; + for (let i = 0; i < lines.length; i++) { + if (/^export\s+(async\s+)?function|^export\s+const\s+\w+\s*=\s*(async\s+)?\(/.test(lines[i])) { + const preceding = lines.slice(Math.max(0, i - 3), i).join("\n"); + if (/\/\*\*/.test(preceding)) { + documentedCount++; + } else { + undocumentedCount++; + } + } + } + const total = documentedCount + undocumentedCount; + return { documentedCount, undocumentedCount, coverage: total > 0 ? documentedCount / total : 0 }; + }, +}); + +// Agent role: check JSDoc coverage for exported functions across TypeScript files. +const tsJsdocCoverageChecker = agent({ + model: "small", + instructions: p`Check JSDoc documentation coverage for exported TypeScript functions. + +TypeScript source files: +${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' | head -20")} + +For each TypeScript file found, call analyzeFunctionComments with the file path. +Build a files record keyed by file path with documentedCount, undocumentedCount, and coverage. +Compute overall: totalFunctions (sum of all), documentedFunctions (sum of documented), coveragePercent (as 0-100). +List wellDocumentedFiles: files where coverage >= 0.8 (as paths).`, + tools: [analyzeFunctionComments], + output: s.object({ + files: s.record( + s.object({ + documentedCount: s.int, + undocumentedCount: s.int, + coverage: s.number, + }) + ), + overall: s.object({ + totalFunctions: s.int, + documentedFunctions: s.int, + coveragePercent: s.number, + }), + wellDocumentedFiles: s.array(s.path), + }), + maxTurns: 6, + addons: repair(), +}); + +export default tsJsdocCoverageChecker; + +``` diff --git a/skills/rig/samples/373-git-hook-installer.md b/skills/rig/samples/373-git-hook-installer.md new file mode 100644 index 0000000..e2b8b01 --- /dev/null +++ b/skills/rig/samples/373-git-hook-installer.md @@ -0,0 +1,52 @@ +# 373 - Git Hook Installer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const checkHooksDir = defineTool("checkHooksDir", { + description: "Check whether .git/hooks directory exists and is writable.", + parameters: s.object({}), + async handler() { + const { access, constants } = await import("node:fs/promises"); + try { + await access(".git/hooks", constants.W_OK); + return { exists: true, writable: true }; + } catch { + return { exists: false, writable: false }; + } + }, +}); + +// Agent role: install git hooks into the .git/hooks directory from the provided hook specs. +const gitHookInstaller = agent({ + model: "small", + input: s.object({ + hooks: s.array( + s.object({ + name: s.string, + script: s.string, + description: s.string, + }) + ), + }), + instructions: p`Install git hooks into .git/hooks from the provided input. + +First call checkHooksDir to verify the hooks directory is accessible. +For each hook in input.hooks, write the script to .git/hooks/ using p.write. +Track which hooks were written successfully and which were skipped (if directory not found). +Return writtenHooks (names of installed hooks), skippedHooks (names not installed), +totalWritten (count of written), and allWritten (true if writtenHooks.length === input.hooks.length).`, + tools: [checkHooksDir], + output: s.object({ + writtenHooks: s.array(s.string), + skippedHooks: s.array(s.string), + totalWritten: s.int, + allWritten: s.boolean, + }), + maxTurns: 6, + addons: repair(), +}); + +export default gitHookInstaller; + +``` diff --git a/skills/rig/samples/374-ts-type-guard-generator.md b/skills/rig/samples/374-ts-type-guard-generator.md new file mode 100644 index 0000000..e280716 --- /dev/null +++ b/skills/rig/samples/374-ts-type-guard-generator.md @@ -0,0 +1,46 @@ +# 374 - TS Type Guard Generator + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const extractInterfaces = defineTool("extractInterfaces", { + description: "Extract interface names from a TypeScript file using regex.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }) { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(filePath, "utf8"); + const matches = [...content.matchAll(/^(?:export\s+)?interface\s+(\w+)/gm)]; + return matches.map((m: RegExpMatchArray) => m[1]); + }, +}); + +// Agent role: generate TypeScript type guard functions for interfaces found in a source file. +const tsTypeGuardGenerator = agent({ + model: "small", + input: s.object({ + sourceFile: s.path, + outputFile: s.path, + }), + instructions: p`Generate TypeScript type guard functions for interfaces in the source file. + +Source file contents: +${p.readInput("sourceFile")} + +1. Call extractInterfaces with the sourceFile path to get the list of interface names. +2. For each interface, generate a type guard function: \`export function is(val: unknown): val is { ... }\` +3. Write all generated type guards as valid TypeScript source to the output field "generatedSource". +4. Return generatedGuards (list of interface names), outputFile (the outputFile from input), totalGenerated (count).`, + tools: [extractInterfaces], + output: s.object({ + generatedGuards: s.array(s.string), + outputFile: s.path, + totalGenerated: s.int, + generatedSource: s.string, + }), + maxTurns: 5, + addons: repair(), +}); + +export default tsTypeGuardGenerator; + +``` diff --git a/skills/rig/samples/375-json-fixture-anonymizer.md b/skills/rig/samples/375-json-fixture-anonymizer.md new file mode 100644 index 0000000..b709fc6 --- /dev/null +++ b/skills/rig/samples/375-json-fixture-anonymizer.md @@ -0,0 +1,53 @@ +# 375 - JSON Fixture Anonymizer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const anonymizeValue = defineTool("anonymizeValue", { + description: "Anonymize a value based on field name heuristics.", + parameters: s.object({ fieldName: s.string, value: s.string }), + handler({ fieldName, value }) { + const name = fieldName.toLowerCase(); + if (/email|mail/.test(name)) return "anonymized@example.com"; + if (/password|secret|token|key|auth/.test(name)) return "***REDACTED***"; + if (/name|user|first|last/.test(name)) return "Anonymous"; + if (/phone|mobile|tel/.test(name)) return "+1-000-000-0000"; + if (/address|street|city|zip|postal/.test(name)) return "123 Redacted St"; + if (/ssn|id|number/.test(name)) return "XXX-XX-XXXX"; + return value; + }, +}); + +// Agent role: anonymize sensitive fields in a JSON fixture file. +const jsonFixtureAnonymizer = agent({ + model: "small", + input: s.object({ + inputFile: s.path, + outputFile: s.path, + fieldsToAnonymize: s.array(s.string), + }), + instructions: p`Anonymize sensitive fields in the JSON fixture file. + +Input file contents: +${p.readInput("inputFile")} + +For each field listed in input.fieldsToAnonymize, call anonymizeValue with the field name and its value. +Also apply anonymization heuristics to any other fields with sensitive-sounding names (email, password, token, name, etc.). +Write the anonymized JSON to the "result" output field. +Return fieldsAnonymized (count of fields changed), totalRecords (if array: length; if object: 1), +outputPath (same as input.outputFile), anonymizedFields (list of field names that were changed).`, + tools: [anonymizeValue], + output: s.object({ + fieldsAnonymized: s.int, + totalRecords: s.int, + outputPath: s.path, + anonymizedFields: s.array(s.string), + result: s.string, + }), + maxTurns: 4, + addons: repair(), +}); + +export default jsonFixtureAnonymizer; + +``` diff --git a/skills/rig/samples/376-csv-to-markdown-table.md b/skills/rig/samples/376-csv-to-markdown-table.md new file mode 100644 index 0000000..5cf93be --- /dev/null +++ b/skills/rig/samples/376-csv-to-markdown-table.md @@ -0,0 +1,61 @@ +# 376 - CSV to Markdown Table + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const parseCSVRow = defineTool("parseCSVRow", { + description: "Parse a single CSV row into an array of cell values, handling quoted fields.", + parameters: s.object({ row: s.string, delimiter: s.string }), + handler({ row, delimiter }) { + const cells: string[] = []; + let current = ""; + let inQuotes = false; + for (let i = 0; i < row.length; i++) { + const ch = row[i]; + if (ch === '"') { + inQuotes = !inQuotes; + } else if (ch === delimiter && !inQuotes) { + cells.push(current.trim()); + current = ""; + } else { + current += ch; + } + } + cells.push(current.trim()); + return cells; + }, +}); + +// Agent role: convert a CSV file to a Markdown table with optional statistics. +const csvToMarkdownTable = agent({ + model: "small", + input: s.object({ + csvFile: s.path, + outputFile: s.path, + includeStats: s.boolean, + }), + instructions: p`Convert the CSV file to a Markdown table. + +CSV file contents: +${p.readInput("csvFile")} + +1. Use parseCSVRow to parse the header row (first line) and each data row, using "," as delimiter. +2. Build a Markdown table with the header row and all data rows. +3. If input.includeStats is true, append a stats section with row count and column count. +4. Write the complete Markdown to the "markdownTable" output field. +5. Return rowCount (data rows only, not header), columnCount, outputFile (from input), headers (list of column names).`, + tools: [parseCSVRow], + output: s.object({ + rowCount: s.int, + columnCount: s.int, + outputFile: s.path, + headers: s.array(s.string), + markdownTable: s.string, + }), + maxTurns: 4, + addons: repair(), +}); + +export default csvToMarkdownTable; + +``` diff --git a/skills/rig/samples/377-ts-interface-method-counter.md b/skills/rig/samples/377-ts-interface-method-counter.md new file mode 100644 index 0000000..ebe81b4 --- /dev/null +++ b/skills/rig/samples/377-ts-interface-method-counter.md @@ -0,0 +1,58 @@ +# 377 - TS Interface Method Counter + +```rig +import { agent, p, s, defineTool, steering } from "rig"; + +const countInterfaceMethods = defineTool("countInterfaceMethods", { + description: "Count method signatures in TypeScript interfaces within a file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }) { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(filePath, "utf8"); + const results: Record = {}; + const ifaceRe = /interface\s+(\w+)[^{]*\{([^}]*)\}/gs; + let match: RegExpExecArray | null; + while ((match = ifaceRe.exec(content)) !== null) { + const name = match[1]; + const body = match[2]; + const methods = (body.match(/\w+\??\s*\([^)]*\)/g) || []); + const hasOptional = /\w+\?\s*\(/.test(body); + results[name] = { methodCount: methods.length, hasOptionalMethods: hasOptional, sourceFile: filePath }; + } + return results; + }, +}); + +// Agent role: count method signatures in TypeScript interfaces across the source tree. +const tsInterfaceMethodCounter = agent({ + model: "small", + instructions: p`Count method signatures in TypeScript interfaces. + +TypeScript source files: +${p.glob("src/**/*.ts")} + +For each file path listed, call countInterfaceMethods to extract interface method counts. +Merge all results into a single interfaces record keyed by interface name. +Compute totalInterfaces (total count of interface names found). +Compute averageMethodCount (total methods / totalInterfaces, or 0 if none). +Set largestInterface to the interface name with the most methods, or omit if no interfaces found.`, + tools: [countInterfaceMethods], + output: s.object({ + interfaces: s.record( + s.object({ + methodCount: s.int, + hasOptionalMethods: s.boolean, + sourceFile: s.string, + }) + ), + totalInterfaces: s.int, + averageMethodCount: s.number, + largestInterface: s.optional(s.string), + }), + maxTurns: 6, + addons: steering({ message: "Ensure every interface found is included in the interfaces record." }), +}); + +export default tsInterfaceMethodCounter; + +``` diff --git a/skills/rig/samples/378-git-tag-annotation-extractor.md b/skills/rig/samples/378-git-tag-annotation-extractor.md new file mode 100644 index 0000000..96d2382 --- /dev/null +++ b/skills/rig/samples/378-git-tag-annotation-extractor.md @@ -0,0 +1,52 @@ +# 378 - Git Tag Annotation Extractor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const classifyTagType = defineTool("classifyTagType", { + description: "Classify a git tag as annotated, lightweight, or signed based on its metadata.", + parameters: s.object({ + tagName: s.string, + objectType: s.string, + taggerDate: s.string, + subject: s.string, + }), + handler({ objectType, taggerDate, subject }) { + if (/BEGIN PGP/.test(subject)) return { type: "signed" as const }; + if (objectType === "tag" || taggerDate.length > 0) return { type: "annotated" as const }; + return { type: "lightweight" as const }; + }, +}); + +// Agent role: extract and classify git tag annotations from the repository. +const gitTagAnnotationExtractor = agent({ + model: "small", + instructions: p`Extract and classify git tag annotations. + +Git tag listing with metadata: +${p.bash("git tag -l --format='%(refname:short)|%(objecttype)|%(contents:subject)|%(taggerdate:short)' | head -30")} + +For each line in the output, split by "|" to get tagName, objectType, subject, taggerDate. +Call classifyTagType for each tag to determine if it is annotated, lightweight, or signed. +Build a tags record keyed by tag name with type, subject (optional, omit if empty), and date (optional, omit if empty). +Count annotatedCount (type === "annotated"), lightweightCount (type === "lightweight"), totalTags (all tags).`, + tools: [classifyTagType], + output: s.object({ + tags: s.record( + s.object({ + type: s.enum("annotated", "lightweight", "signed"), + subject: s.optional(s.string), + date: s.optional(s.string), + }) + ), + annotatedCount: s.int, + lightweightCount: s.int, + totalTags: s.int, + }), + maxTurns: 4, + addons: repair(), +}); + +export default gitTagAnnotationExtractor; + +``` diff --git a/skills/rig/samples/379-jsonl-file-analyzer.md b/skills/rig/samples/379-jsonl-file-analyzer.md new file mode 100644 index 0000000..5457216 --- /dev/null +++ b/skills/rig/samples/379-jsonl-file-analyzer.md @@ -0,0 +1,56 @@ +# 379 - JSONL File Analyzer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const analyzeJsonLine = defineTool("analyzeJsonLine", { + description: "Parse a single JSONL line and return its structure.", + parameters: s.object({ line: s.string }), + handler({ line }) { + try { + const obj = JSON.parse(line); + if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { + return { valid: true, keys: [], valueTypes: {} as Record }; + } + const keys = Object.keys(obj); + const valueTypes: Record = {}; + for (const k of keys) { + valueTypes[k] = Array.isArray(obj[k]) ? "array" : typeof obj[k]; + } + return { valid: true, keys, valueTypes }; + } catch { + return { valid: false, keys: [], valueTypes: {} as Record }; + } + }, +}); + +// Agent role: analyze a JSONL file to report line validity and schema consistency. +const jsonlFileAnalyzer = agent({ + model: "small", + input: s.object({ + inputFile: s.path, + }), + instructions: p`Analyze the JSONL (JSON Lines) file. + +File contents: +${p.readInput("inputFile")} + +Split the content by newlines. For each non-empty line, call analyzeJsonLine. +Track totalLines (all non-empty lines), validLines (lines that parse successfully), invalidLines (failed). +Collect all unique keys across valid lines and return the 10 most frequent as topKeys. +Set schemaConsistent to true if all valid lines share the exact same set of top-level keys.`, + tools: [analyzeJsonLine], + output: s.object({ + totalLines: s.int, + validLines: s.int, + invalidLines: s.int, + topKeys: s.array(s.string), + schemaConsistent: s.boolean, + }), + maxTurns: 4, + addons: repair(), +}); + +export default jsonlFileAnalyzer; + +``` diff --git a/skills/rig/samples/380-npm-peer-dep-conflict-checker.md b/skills/rig/samples/380-npm-peer-dep-conflict-checker.md new file mode 100644 index 0000000..d66b561 --- /dev/null +++ b/skills/rig/samples/380-npm-peer-dep-conflict-checker.md @@ -0,0 +1,62 @@ +# 380 - NPM Peer Dep Conflict Checker + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const checkPeerConflict = defineTool("checkPeerConflict", { + description: "Check whether a package version satisfies the expected peer dependency range.", + parameters: s.object({ + packageName: s.string, + expectedRange: s.string, + foundVersion: s.string, + }), + handler({ expectedRange, foundVersion }) { + if (!foundVersion) { + return { conflicting: true, reason: "Package not installed", severity: "error" as const }; + } + // Simple semver major check + const expectedMajor = parseInt(expectedRange.replace(/[^0-9]/, ""), 10); + const foundMajor = parseInt(foundVersion.replace(/[^0-9]/, ""), 10); + if (!isNaN(expectedMajor) && !isNaN(foundMajor) && foundMajor !== expectedMajor) { + return { conflicting: true, reason: `Expected major ${expectedMajor}, found ${foundMajor}`, severity: "error" as const }; + } + return { conflicting: false, reason: undefined, severity: "ok" as const }; + }, +}); + +// Agent role: identify peer dependency conflicts in the current npm project. +const npmPeerDepConflictChecker = agent({ + model: "small", + instructions: p`Check for peer dependency conflicts in the npm project. + +package.json: +${p.read("package.json")} + +npm dependency tree (may contain WARN lines about peer conflicts): +${p.bash("npm ls --json 2>&1 | head -200")} + +For each peerDependency entry in package.json, identify what version is actually installed from the npm ls output. +Call checkPeerConflict with packageName, expectedRange (from peerDependencies), and foundVersion (from npm ls or empty string if missing). +Return conflicts (array of packages with conflicting status), totalConflicts (count of conflicting: true), +hasErrors (any severity === "error"), hasPeerDeps (peerDependencies field exists and is non-empty).`, + tools: [checkPeerConflict], + output: s.object({ + conflicts: s.array( + s.object({ + package: s.string, + expected: s.string, + found: s.optional(s.string), + severity: s.enum("error", "warning", "ok"), + }) + ), + totalConflicts: s.int, + hasErrors: s.boolean, + hasPeerDeps: s.boolean, + }), + maxTurns: 5, + addons: repair(), +}); + +export default npmPeerDepConflictChecker; + +```