diff --git a/skills/rig/samples/371-git-file-ownership-mapper.md b/skills/rig/samples/371-git-file-ownership-mapper.md new file mode 100644 index 0000000..f82e498 --- /dev/null +++ b/skills/rig/samples/371-git-file-ownership-mapper.md @@ -0,0 +1,54 @@ +# 371 - Git File Ownership Mapper + +```rig +import { agent, p, s, defineTool, steering } from "rig"; +import { execSync } from "node:child_process"; + +const getFileOwner = defineTool("getFileOwner", { + description: "Get git commit history for a file and return ownership info.", + parameters: { filePath: s.path }, + handler: ({ filePath }: { filePath: string }) => { + try { + const output = execSync(`git log --format="%ae" -- "${filePath}" 2>/dev/null`, { encoding: "utf-8" }).trim(); + const emails = output ? output.split("\n").filter(Boolean) : []; + const counts: Record = {}; + for (const email of emails) counts[email] = (counts[email] ?? 0) + 1; + const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]); + const primaryOwner = sorted[0]?.[0] ?? "unknown"; + const commitCount = emails.length; + const contributors = sorted.map(([email]) => email); + return { primaryOwner, commitCount, contributors }; + } catch { + return { primaryOwner: "unknown", commitCount: 0, contributors: [] }; + } + }, +}); + +// Agent role: map git file ownership for all TypeScript source files. +const gitFileOwnershipMapper = agent({ + model: "small", + instructions: p`Map git file ownership for TypeScript source files. + +Source files: +${p.glob("src/**/*.ts")} + +Steps: +1. For each file path listed above, call getFileOwner to retrieve primaryOwner, commitCount, contributors. +2. Build ownership record keyed by file path. +3. Find mostActiveContributor: the email with the highest total commit count across all files.`, + output: s.object({ + ownership: s.record( + s.object({ + primaryOwner: s.string, + commitCount: s.int, + contributors: s.array(s.string), + }) + ), + mostActiveContributor: s.string, + }), + tools: [getFileOwner], + addons: [steering()], +}); + +export default gitFileOwnershipMapper; +``` diff --git a/skills/rig/samples/372-workflow-input-validator.md b/skills/rig/samples/372-workflow-input-validator.md new file mode 100644 index 0000000..dd3ec8f --- /dev/null +++ b/skills/rig/samples/372-workflow-input-validator.md @@ -0,0 +1,64 @@ +# 372 - Workflow Input Validator + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const validateWorkflowInputs = defineTool("validateWorkflowInputs", { + description: "Parse a GitHub Actions workflow YAML file and extract workflow_dispatch inputs.", + parameters: { filePath: s.path }, + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf-8"); + const inputsMatch = content.match(/workflow_dispatch:\s*\n(?:\s+.*\n)*?\s+inputs:([\s\S]*?)(?=\n\w|\n\s{0,2}\w|$)/); + if (!inputsMatch) return { inputs: {}, inputCount: 0, hasRequiredWithoutDefault: false }; + const inputsBlock = inputsMatch[1]; + const inputEntries: Record = {}; + const inputPattern = /^\s{4,8}(\w+):\s*\n((?:\s{6,12}.+\n?)*)/gm; + let match: RegExpExecArray | null; + while ((match = inputPattern.exec(inputsBlock)) !== null) { + const name = match[1]; + const block = match[2]; + const typeM = block.match(/type:\s*(.+)/); + const requiredM = block.match(/required:\s*(true|false)/); + const defaultM = block.match(/default:\s*(.+)/); + inputEntries[name] = { + type: typeM ? typeM[1].trim() : "string", + required: requiredM ? requiredM[1] === "true" : false, + default: defaultM ? defaultM[1].trim() : undefined, + }; + } + const inputCount = Object.keys(inputEntries).length; + const hasRequiredWithoutDefault = Object.values(inputEntries).some( + (v) => v.required && v.default === undefined + ); + return { inputs: inputEntries, inputCount, hasRequiredWithoutDefault }; + }, +}); + +// Agent role: validate GitHub Actions workflow_dispatch inputs across all workflow files. +const workflowInputValidator = agent({ + model: "small", + instructions: p`Validate workflow_dispatch inputs in all GitHub Actions workflow files. + +Workflow files: +${p.glob(".github/workflows/*.yml")} + +Steps: +1. For each file path listed above, call validateWorkflowInputs to extract inputs, inputCount, hasRequiredWithoutDefault. +2. Build workflows record keyed by filename. +3. Set totalWorkflows to the count of workflow files processed.`, + output: s.object({ + workflows: s.record( + s.object({ + inputCount: s.int, + hasRequiredWithoutDefault: s.boolean, + }) + ), + totalWorkflows: s.int, + }), + tools: [validateWorkflowInputs], + addons: [repair()], +}); + +export default workflowInputValidator; +``` diff --git a/skills/rig/samples/373-ts-dead-export-finder.md b/skills/rig/samples/373-ts-dead-export-finder.md new file mode 100644 index 0000000..fbf3e77 --- /dev/null +++ b/skills/rig/samples/373-ts-dead-export-finder.md @@ -0,0 +1,59 @@ +# 373 - TypeScript Dead Export Finder + +```rig +import { agent, p, s, defineTool, steering, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const findUnusedExports = defineTool("findUnusedExports", { + description: "Find exported symbols in a TypeScript file that are not imported in other files.", + parameters: { filePath: s.path, allFiles: s.array(s.string) }, + handler: async ({ filePath, allFiles }: { filePath: string; allFiles: string[] }) => { + const content = await readFile(filePath, "utf-8"); + const exportPattern = /export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/g; + const exported: string[] = []; + let m: RegExpExecArray | null; + while ((m = exportPattern.exec(content)) !== null) exported.push(m[1]); + const unused: string[] = []; + for (const sym of exported) { + let found = false; + for (const other of allFiles) { + if (other === filePath) continue; + try { + const otherContent = await readFile(other, "utf-8"); + if (otherContent.includes(sym)) { found = true; break; } + } catch { /* skip */ } + } + if (!found) unused.push(sym); + } + return { unused }; + }, +}); + +// Agent role: find unused TypeScript exports across the codebase. +const tsDeadExportFinder = agent({ + model: "small", + instructions: p`Find TypeScript exported symbols that are never imported elsewhere. + +TypeScript files: +${p.glob("src/**/*.ts")} + +All TypeScript files (for cross-reference): +${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' 2>/dev/null | head -100")} + +Steps: +1. For each file in the source list, call findUnusedExports with that file and the full allFiles list. +2. Build unusedExports record keyed by file path, value is array of unused symbol names. +3. Omit files with no unused exports. +4. totalUnused = total count of unused symbols across all files. +5. hasDeadCode = totalUnused > 0.`, + output: s.object({ + unusedExports: s.record(s.array(s.string)), + totalUnused: s.int, + hasDeadCode: s.boolean, + }), + tools: [findUnusedExports], + addons: [steering(), repair()], +}); + +export default tsDeadExportFinder; +``` diff --git a/skills/rig/samples/374-package-scripts-documenter.md b/skills/rig/samples/374-package-scripts-documenter.md new file mode 100644 index 0000000..3d5d603 --- /dev/null +++ b/skills/rig/samples/374-package-scripts-documenter.md @@ -0,0 +1,54 @@ +# 374 - Package Scripts Documenter + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const inferScriptPurpose = defineTool("inferScriptPurpose", { + description: "Classify a package.json script by its command into a category.", + parameters: { name: s.string, command: s.string }, + handler: ({ name, command }: { name: string; command: string }) => { + const cmd = command.toLowerCase(); + const nm = name.toLowerCase(); + let category: "build" | "test" | "lint" | "release" | "dev" | "other" = "other"; + if (/\btest\b|jest|vitest|mocha/.test(cmd) || /\btest/.test(nm)) category = "test"; + else if (/\bbuild\b|tsc|webpack|vite|rollup|esbuild/.test(cmd) || /\bbuild/.test(nm)) category = "build"; + else if (/\blint\b|eslint|prettier|biome/.test(cmd) || /\blint/.test(nm)) category = "lint"; + else if (/\brelease\b|publish|changeset|version/.test(cmd) || /\brelease\b|\bpublish/.test(nm)) category = "release"; + else if (/\bdev\b|watch|start\b|nodemon/.test(cmd) || /\bdev\b|\bstart\b|\bwatch/.test(nm)) category = "dev"; + const purpose = `Runs ${name}: ${command.slice(0, 60)}`; + return { purpose, category } as const; + }, +}); + +// Agent role: document all package.json scripts with purpose and category, then write SCRIPTS.md. +const packageScriptsDocumenter = agent({ + model: "small", + instructions: p`Document all scripts in package.json and write SCRIPTS.md. + +package.json contents: +${p.read("package.json")} + +Steps: +1. Parse the scripts object from the package.json content above. +2. For each script name and command, call inferScriptPurpose to get purpose and category. +3. Build the scripts record keyed by script name. +4. Write SCRIPTS.md using p.write with a markdown table listing each script, its category, and purpose. +5. documentedCount = number of scripts processed. +6. outputFile = "SCRIPTS.md".`, + output: s.object({ + scripts: s.record( + s.object({ + purpose: s.string, + category: s.enum("build", "test", "lint", "release", "dev", "other"), + command: s.string, + }) + ), + documentedCount: s.int, + outputFile: s.path, + }), + tools: [inferScriptPurpose], + addons: [repair()], +}); + +export default packageScriptsDocumenter; +``` diff --git a/skills/rig/samples/375-git-diff-stats-summarizer.md b/skills/rig/samples/375-git-diff-stats-summarizer.md new file mode 100644 index 0000000..6a5f498 --- /dev/null +++ b/skills/rig/samples/375-git-diff-stats-summarizer.md @@ -0,0 +1,51 @@ +# 375 - Git Diff Stats Summarizer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const classifyDiffEntry = defineTool("classifyDiffEntry", { + description: "Classify a git diff --numstat entry as added, modified, deleted, or renamed.", + parameters: { additions: s.int, deletions: s.int, path: s.string }, + handler: ({ additions, deletions, path }: { additions: number; deletions: number; path: string }) => { + let changeType: "added" | "modified" | "deleted" | "renamed" = "modified"; + if (path.includes(" => ") || path.includes("{")) changeType = "renamed"; + else if (deletions === 0 && additions > 0) changeType = "added"; + else if (additions === 0 && deletions > 0) changeType = "deleted"; + return { changeType } as const; + }, +}); + +// Agent role: summarize git diff statistics between HEAD~1 and HEAD. +const gitDiffStatsSummarizer = agent({ + model: "small", + instructions: p`Summarize file changes between the last two commits. + +Diff numstat output: +${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null || echo ''")} + +Steps: +1. Parse each line of the numstat output (format: additions TAB deletions TAB path). +2. For each entry, call classifyDiffEntry with additions, deletions, path to get changeType. +3. Build files array with path, additions, deletions, changeType. +4. totalAdditions = sum of all additions. +5. totalDeletions = sum of all deletions. +6. mostChangedFile = path with highest additions+deletions (omit if no files).`, + output: s.object({ + files: s.array( + s.object({ + path: s.string, + additions: s.int, + deletions: s.int, + changeType: s.enum("added", "modified", "deleted", "renamed"), + }) + ), + totalAdditions: s.int, + totalDeletions: s.int, + mostChangedFile: s.optional(s.string), + }), + tools: [classifyDiffEntry], + addons: [repair()], +}); + +export default gitDiffStatsSummarizer; +``` diff --git a/skills/rig/samples/376-dotenv-template-generator.md b/skills/rig/samples/376-dotenv-template-generator.md new file mode 100644 index 0000000..5d8c52b --- /dev/null +++ b/skills/rig/samples/376-dotenv-template-generator.md @@ -0,0 +1,53 @@ +# 376 - Dotenv Template Generator + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const extractEnvReferences = defineTool("extractEnvReferences", { + description: "Extract process.env.X references from a TypeScript file.", + parameters: { filePath: s.path }, + handler: async ({ filePath }: { filePath: string }) => { + try { + const content = await readFile(filePath, "utf-8"); + const pattern = /process\.env\.([A-Z_][A-Z0-9_]*)/g; + const keys = new Set(); + let m: RegExpExecArray | null; + while ((m = pattern.exec(content)) !== null) keys.add(m[1]); + return { keys: Array.from(keys) }; + } catch { + return { keys: [] }; + } + }, +}); + +// Agent role: generate a .env.template from process.env references in source files. +const dotenvTemplateGenerator = agent({ + model: "small", + instructions: p`Generate a .env.template file from process.env references in TypeScript source files. + +Existing .env file (if present): +${p.readOptional(".env", "(no .env file found)")} + +TypeScript source files: +${p.glob("src/**/*.ts")} + +Steps: +1. For each TypeScript file path listed above, call extractEnvReferences to get the list of env keys. +2. Collect all unique keys referenced across all files → envKeys. +3. Parse the existing .env content to find documented keys. +4. undocumentedKeys = envKeys not already in .env. +5. Write .env.template with each key as KEY= (one per line, with a comment header). +6. templatePath = ".env.template", templateGenerated = true.`, + output: s.object({ + templatePath: s.path, + envKeys: s.array(s.string), + undocumentedKeys: s.array(s.string), + templateGenerated: s.boolean, + }), + tools: [extractEnvReferences], + addons: [repair()], +}); + +export default dotenvTemplateGenerator; +``` diff --git a/skills/rig/samples/377-toml-config-extractor.md b/skills/rig/samples/377-toml-config-extractor.md new file mode 100644 index 0000000..54de769 --- /dev/null +++ b/skills/rig/samples/377-toml-config-extractor.md @@ -0,0 +1,65 @@ +# 377 - TOML Config Extractor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const parseTomlSection = defineTool("parseTomlSection", { + description: "Parse TOML content and return sections with key-value pairs.", + parameters: { content: s.string }, + handler: ({ content }: { content: string }) => { + const lines = content.split("\n"); + const sections: Record> = {}; + let currentSection = "__default__"; + let hasDefaultSection = false; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/); + if (sectionMatch) { + currentSection = sectionMatch[1]; + sections[currentSection] = sections[currentSection] ?? {}; + continue; + } + const kvMatch = trimmed.match(/^([^=]+?)\s*=\s*(.+)$/); + if (kvMatch) { + if (currentSection === "__default__") { + sections[currentSection] = sections[currentSection] ?? {}; + hasDefaultSection = true; + } + sections[currentSection][kvMatch[1].trim()] = kvMatch[2].trim().replace(/^["']|["']$/g, ""); + } + } + const totalSections = Object.keys(sections).filter((k) => k !== "__default__").length; + const totalKeys = Object.values(sections).reduce((sum, s) => sum + Object.keys(s).length, 0); + const result: Record> = {}; + for (const [sec, kvs] of Object.entries(sections)) { + if (sec !== "__default__" || Object.keys(kvs).length > 0) result[sec] = kvs; + } + return { sections: result, totalKeys, totalSections, hasDefaultSection }; + }, +}); + +// Agent role: extract all sections and key-value pairs from a TOML config file. +const tomlConfigExtractor = agent({ + model: "small", + input: s.object({ configFile: s.path }), + instructions: p`Extract sections and key-value pairs from a TOML config file. + +File contents: +${p.readInput("configFile")} + +Steps: +1. Call parseTomlSection with the full file content to get sections, totalKeys, totalSections, hasDefaultSection. +2. Return the result directly.`, + output: s.object({ + sections: s.record(s.record(s.string)), + totalKeys: s.int, + totalSections: s.int, + hasDefaultSection: s.boolean, + }), + tools: [parseTomlSection], + addons: [repair()], +}); + +export default tomlConfigExtractor; +``` diff --git a/skills/rig/samples/378-csv-column-stats.md b/skills/rig/samples/378-csv-column-stats.md new file mode 100644 index 0000000..0638dd7 --- /dev/null +++ b/skills/rig/samples/378-csv-column-stats.md @@ -0,0 +1,60 @@ +# 378 - CSV Column Stats Reporter + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const analyzeColumn = defineTool("analyzeColumn", { + description: "Analyze a CSV column's values and return statistics.", + parameters: { columnName: s.string, values: s.array(s.string) }, + handler: ({ columnName, values }: { columnName: string; values: string[] }) => { + const numericValues = values.map((v) => parseFloat(v)).filter((n) => !isNaN(n)); + const uniqueCount = new Set(values).size; + const isAllNumeric = numericValues.length === values.filter((v) => v.trim() !== "").length; + const isMixed = numericValues.length > 0 && !isAllNumeric; + const type: "numeric" | "string" | "mixed" = isAllNumeric ? "numeric" : isMixed ? "mixed" : "string"; + if (type === "numeric" && numericValues.length > 0) { + const min = Math.min(...numericValues); + const max = Math.max(...numericValues); + const mean = numericValues.reduce((a, b) => a + b, 0) / numericValues.length; + return { columnName, type, uniqueCount, min, max, mean }; + } + return { columnName, type, uniqueCount, min: undefined, max: undefined, mean: undefined }; + }, +}); + +// Agent role: compute per-column statistics for a CSV file. +const csvColumnStatsReporter = agent({ + model: "small", + input: s.object({ csvFile: s.path }), + instructions: p`Compute statistics for each column in a CSV file. + +CSV contents: +${p.readInput("csvFile")} + +Steps: +1. Parse the first line as the header row (comma-separated column names). +2. Parse remaining lines as data rows. +3. For each column, collect its values from all data rows. +4. Call analyzeColumn with the columnName and its values array. +5. Build columns record keyed by column name. +6. rowCount = number of data rows (excluding header). +7. columnCount = number of columns.`, + output: s.object({ + columns: s.record( + s.object({ + type: s.enum("numeric", "string", "mixed"), + uniqueCount: s.int, + min: s.optional(s.number), + max: s.optional(s.number), + mean: s.optional(s.number), + }) + ), + rowCount: s.int, + columnCount: s.int, + }), + tools: [analyzeColumn], + addons: [repair()], +}); + +export default csvColumnStatsReporter; +``` diff --git a/skills/rig/samples/379-git-worktree-analyzer.md b/skills/rig/samples/379-git-worktree-analyzer.md new file mode 100644 index 0000000..85f0f64 --- /dev/null +++ b/skills/rig/samples/379-git-worktree-analyzer.md @@ -0,0 +1,51 @@ +# 379 - Git Worktree Analyzer + +```rig +import { agent, p, s, defineTool, steering } from "rig"; + +const classifyWorktree = defineTool("classifyWorktree", { + description: "Classify a git worktree entry by type and status.", + parameters: { path: s.string, branch: s.string, commit: s.string, bare: s.boolean, detached: s.boolean }, + handler: ({ path, branch, commit, bare, detached }: { path: string; branch: string; commit: string; bare: boolean; detached: boolean }) => { + const type: "main" | "linked" | "bare" = bare ? "bare" : path === process.cwd() ? "main" : "linked"; + const status: "clean" | "dirty" | "detached" = detached ? "detached" : "clean"; + return { path, branch: branch || undefined, commit, type, status } as const; + }, +}); + +// Agent role: analyze git worktrees and classify each by type and status. +const gitWorktreeAnalyzer = agent({ + model: "small", + instructions: p`Analyze all git worktrees in this repository. + +Worktree listing: +${p.bash("git worktree list --porcelain 2>/dev/null || echo ''")} + +Steps: +1. Parse the porcelain output into worktree blocks (each block separated by blank lines). +2. From each block extract: worktree path (first field), HEAD commit, branch (refs/heads/NAME or detached), and bare/detached flags. +3. Call classifyWorktree for each block to get type and status. +4. Build worktrees array. +5. totalWorktrees = worktrees.length. +6. hasLinked = any worktree with type "linked". +7. dirtyCount = count of worktrees with status "dirty".`, + output: s.object({ + worktrees: s.array( + s.object({ + path: s.string, + branch: s.optional(s.string), + commit: s.string, + type: s.enum("main", "linked", "bare"), + status: s.enum("clean", "dirty", "detached"), + }) + ), + totalWorktrees: s.int, + hasLinked: s.boolean, + dirtyCount: s.int, + }), + tools: [classifyWorktree], + addons: [steering()], +}); + +export default gitWorktreeAnalyzer; +``` diff --git a/skills/rig/samples/380-ts-narrowing-detector.md b/skills/rig/samples/380-ts-narrowing-detector.md new file mode 100644 index 0000000..5a07ae1 --- /dev/null +++ b/skills/rig/samples/380-ts-narrowing-detector.md @@ -0,0 +1,56 @@ +# 380 - TypeScript Narrowing Pattern Detector + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const scanNarrowingPatterns = defineTool("scanNarrowingPatterns", { + description: "Count typeof, instanceof, and 'in' operator usages in a TypeScript file.", + parameters: { filePath: s.path }, + handler: async ({ filePath }: { filePath: string }) => { + try { + const content = await readFile(filePath, "utf-8"); + const typeofCount = (content.match(/\btypeof\s+\w+\s*[=!]==/g) ?? []).length; + const instanceofCount = (content.match(/\binstanceof\b/g) ?? []).length; + const inOperatorCount = (content.match(/\b\w+\s+in\s+\w+/g) ?? []).length; + const total = typeofCount + instanceofCount + inOperatorCount; + return { typeofCount, instanceofCount, inOperatorCount, total }; + } catch { + return { typeofCount: 0, instanceofCount: 0, inOperatorCount: 0, total: 0 }; + } + }, +}); + +// Agent role: detect TypeScript type narrowing patterns across source files. +const tsNarrowingDetector = agent({ + model: "small", + instructions: p`Detect type narrowing patterns in TypeScript source files. + +Source files: +${p.glob("src/**/*.ts")} + +Steps: +1. For each file path, call scanNarrowingPatterns to get typeofCount, instanceofCount, inOperatorCount, total. +2. Build files record keyed by file path. +3. totalFiles = number of files scanned. +4. mostNarrowedFile = file path with the highest total (omit if all are zero). +5. totalNarrowingPatterns = sum of total across all files.`, + output: s.object({ + files: s.record( + s.object({ + typeofCount: s.int, + instanceofCount: s.int, + inOperatorCount: s.int, + total: s.int, + }) + ), + totalFiles: s.int, + mostNarrowedFile: s.optional(s.string), + totalNarrowingPatterns: s.int, + }), + tools: [scanNarrowingPatterns], + addons: [repair()], +}); + +export default tsNarrowingDetector; +```