diff --git a/skills/rig/samples/381-ts-dead-export-finder.md b/skills/rig/samples/381-ts-dead-export-finder.md new file mode 100644 index 0000000..a4948a5 --- /dev/null +++ b/skills/rig/samples/381-ts-dead-export-finder.md @@ -0,0 +1,53 @@ +# 381 - TS Dead Export Finder + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFile } from "node:fs/promises"; + +const findUnusedExports = defineTool("findUnusedExports", { + description: "Find exported symbols in a TypeScript file that are not imported elsewhere in the codebase", + parameters: s.object({ + filePath: s.path, + allFilePaths: s.array(s.path), + }), + handler: async ({ filePath, allFilePaths }: { filePath: string; allFilePaths: string[] }) => { + const content = await readFile(filePath, "utf-8"); + const exportMatches = [...content.matchAll(/^export\s+(?:function|const|class|type|interface|enum)\s+(\w+)/gm)]; + const exportedNames = exportMatches.map((m) => m[1]); + + const unusedNames: string[] = []; + for (const name of exportedNames) { + let isImported = false; + for (const other of allFilePaths) { + if (other === filePath) continue; + const otherContent = await readFile(other, "utf-8").catch(() => ""); + if (new RegExp(`\\b${name}\\b`).test(otherContent)) { + isImported = true; + break; + } + } + if (!isImported) unusedNames.push(name); + } + return unusedNames; + }, +}); + +// Agent role: Scan TypeScript source files and identify exported symbols that are never imported elsewhere. +const tsDeadExportFinder = agent({ + model: "small", + instructions: p`You are a dead code analyzer. +Discovered TypeScript files: ${p.glob("src/**/*.ts")} +Also check: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/dist/*' | head -50")} + +For each TypeScript file found, call findUnusedExports with that file path and the full list of all TypeScript file paths. +Aggregate results and return the full output schema.`, + output: s.object({ + unusedExports: s.record(s.array(s.string)), + totalUnused: s.int, + hasDeadCode: s.boolean, + }), + tools: [findUnusedExports], +}); + +export default tsDeadExportFinder; +``` diff --git a/skills/rig/samples/382-pkg-scripts-documenter.md b/skills/rig/samples/382-pkg-scripts-documenter.md new file mode 100644 index 0000000..9bf0f34 --- /dev/null +++ b/skills/rig/samples/382-pkg-scripts-documenter.md @@ -0,0 +1,46 @@ +# 382 - Pkg Scripts Documenter + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const inferScriptPurpose = defineTool("inferScriptPurpose", { + description: "Classify an npm script by its command string into a category", + parameters: s.object({ + name: s.string, + command: s.string, + }), + handler: ({ command }: { name: string; command: string }) => { + const lower = command.toLowerCase(); + if (/\btest\b|jest|vitest|mocha/.test(lower)) return "test" as const; + if (/\bbuild\b|tsc|webpack|rollup|vite build/.test(lower)) return "build" as const; + if (/\blint\b|eslint|prettier/.test(lower)) return "lint" as const; + if (/\brelease\b|publish|changeset/.test(lower)) return "release" as const; + if (/\bdev\b|start|watch|serve/.test(lower)) return "dev" as const; + return "other" as const; + }, +}); + +// Agent role: Read package.json scripts, classify each by purpose, and write a SCRIPTS.md documentation file. +const pkgScriptsDocumenter = agent({ + model: "small", + instructions: p`You are a package.json scripts documenter. +Read the package.json: ${p.read("package.json")} + +For each script in the "scripts" field, call inferScriptPurpose to classify it. +Then write a SCRIPTS.md file using ${p.write("SCRIPTS.md", "## Scripts\n\n")} as the write intent path. +Return the complete output schema with all scripts, their purposes, categories, and commands.`, + 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.string, + }), + tools: [inferScriptPurpose], + addons: [repair()], +}); + +export default pkgScriptsDocumenter; +``` diff --git a/skills/rig/samples/383-git-diff-stats-summarizer.md b/skills/rig/samples/383-git-diff-stats-summarizer.md new file mode 100644 index 0000000..c744ddb --- /dev/null +++ b/skills/rig/samples/383-git-diff-stats-summarizer.md @@ -0,0 +1,45 @@ +# 383 - Git Diff Stats Summarizer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const classifyDiffEntry = defineTool("classifyDiffEntry", { + description: "Classify a git diff --numstat line as added, modified, deleted, or renamed", + parameters: s.object({ + line: s.string, + }), + handler: ({ line }: { line: string }) => { + if (line.startsWith("0\t")) return "added" as const; + if (line.includes("\t") && line.split("\t")[0] !== "0" && line.split("\t")[1] !== "0") return "modified" as const; + if (line.split("\t")[0] === "0") return "deleted" as const; + if (line.includes("=>")) return "renamed" as const; + return "modified" as const; + }, +}); + +// Agent role: Summarize git diff statistics between HEAD~1 and HEAD, classifying each changed file. +const gitDiffStatsSummarizer = agent({ + model: "small", + instructions: p`You are a git diff statistics summarizer. +Run: ${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null || git diff --numstat HEAD 2>/dev/null || echo 'no diff available'")} + +For each output line, call classifyDiffEntry to get the category. +Parse additions and deletions counts from each line (format: additions deletions filepath). +Return the full output schema with all file stats.`, + output: s.object({ + files: s.array(s.object({ + path: s.string, + additions: s.int, + deletions: s.int, + category: 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/384-dotenv-template-generator.md b/skills/rig/samples/384-dotenv-template-generator.md new file mode 100644 index 0000000..6850d78 --- /dev/null +++ b/skills/rig/samples/384-dotenv-template-generator.md @@ -0,0 +1,42 @@ +# 384 - Dotenv Template Generator + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const extractEnvReferences = defineTool("extractEnvReferences", { + description: "Extract all process.env.VAR_NAME references from a TypeScript file", + parameters: s.object({ + filePath: s.path, + }), + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf-8").catch(() => ""); + const matches = [...content.matchAll(/process\.env\.([A-Z_][A-Z0-9_]*)/g)]; + return [...new Set(matches.map((m) => m[1]))]; + }, +}); + +// Agent role: Discover all environment variable references in TypeScript source files, compare with .env, and write a .env.template. +const dotenvTemplateGenerator = agent({ + model: "small", + instructions: p`You are a dotenv template generator. +Existing .env file (if any): ${p.readOptional(".env")} +TypeScript source files to scan: ${p.glob("src/**/*.ts")} + +For each TypeScript file found, call extractEnvReferences to get all process.env references. +Collect all unique env keys referenced across all files. +Compare with keys found in .env (lines starting with KEY=). +Write the template to: ${p.write(".env.template", "# Generated .env template")} +Return the output schema.`, + output: s.object({ + templatePath: s.string, + 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/385-ts-class-hierarchy-extractor.md b/skills/rig/samples/385-ts-class-hierarchy-extractor.md new file mode 100644 index 0000000..adb65e7 --- /dev/null +++ b/skills/rig/samples/385-ts-class-hierarchy-extractor.md @@ -0,0 +1,52 @@ +# 385 - TS Class Hierarchy Extractor + +```rig +import { agent, p, s, defineTool, steering } from "rig"; +import { readFile } from "node:fs/promises"; + +const extractClassInfo = defineTool("extractClassInfo", { + description: "Extract class declaration, extends, and implements info from a TypeScript file", + parameters: s.object({ + filePath: s.path, + }), + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf-8").catch(() => ""); + const classRegex = /(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?/g; + const classes: Array<{ name: string; parent: string | null; interfaces: string[]; isAbstract: boolean }> = []; + for (const m of content.matchAll(classRegex)) { + classes.push({ + name: m[1], + parent: m[2] ?? null, + interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()) : [], + isAbstract: content.slice(Math.max(0, m.index! - 10), m.index!).includes("abstract"), + }); + } + return classes; + }, +}); + +// Agent role: Extract class hierarchy information across all TypeScript files and report inheritance depth. +const tsClassHierarchyExtractor = agent({ + model: "small", + instructions: p`You are a TypeScript class hierarchy extractor. +TypeScript files in project: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/dist/*' | head -100")} + +For each TypeScript file found, call extractClassInfo to get class declarations. +Aggregate all classes across files. Compute inheritance depth for each class by tracing parent chains. +Identify root classes (no parent). Return the output schema.`, + output: s.object({ + classes: s.record(s.object({ + parent: s.optional(s.string), + interfaces: s.array(s.string), + isAbstract: s.boolean, + depth: s.int, + })), + maxDepth: s.int, + rootClasses: s.array(s.string), + }), + tools: [extractClassInfo], + addons: [steering()], +}); + +export default tsClassHierarchyExtractor; +``` diff --git a/skills/rig/samples/386-git-bisect-helper.md b/skills/rig/samples/386-git-bisect-helper.md new file mode 100644 index 0000000..ee9c34c --- /dev/null +++ b/skills/rig/samples/386-git-bisect-helper.md @@ -0,0 +1,42 @@ +# 386 - Git Bisect Helper + +```rig +import { agent, p, s, defineTool, steering } from "rig"; + +const selectMidpoint = defineTool("selectMidpoint", { + description: "Select the midpoint commit hash from a list of commit hashes for binary search", + parameters: s.object({ + commits: s.array(s.string), + }), + handler: ({ commits }: { commits: string[] }) => { + if (commits.length === 0) return null; + return commits[Math.floor(commits.length / 2)]; + }, +}); + +// Agent role: Help identify a suspect commit using binary search over the recent git log. +const gitBisectHelper = agent({ + model: "small", + instructions: p`You are a git bisect helper that uses binary search to find a suspect commit. +Recent git log: ${p.bash("git log --oneline -20 2>/dev/null || echo 'no git log available'")} + +Use the selectMidpoint tool to identify candidate commits step by step. +Perform up to 8 binary search steps over the commit list. +After narrowing down, return your best suspect commit, how many steps you took, +the commit range you searched, and your confidence level.`, + output: s.object({ + suspectCommit: s.optional(s.string), + stepsRun: s.int, + commitRange: s.object({ + start: s.string, + end: s.string, + }), + confidence: s.enum("high", "medium", "low"), + }), + tools: [selectMidpoint], + maxTurns: 8, + addons: [steering()], +}); + +export default gitBisectHelper; +``` diff --git a/skills/rig/samples/387-shell-shebang-validator.md b/skills/rig/samples/387-shell-shebang-validator.md new file mode 100644 index 0000000..aa1dbb0 --- /dev/null +++ b/skills/rig/samples/387-shell-shebang-validator.md @@ -0,0 +1,50 @@ +# 387 - Shell Shebang Validator + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const checkShebangLine = defineTool("checkShebangLine", { + description: "Check if a shell script has a valid shebang line on the first line", + parameters: s.object({ + filePath: s.path, + }), + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf-8").catch(() => ""); + const firstLine = content.split("\n")[0] ?? ""; + const hasShebang = firstLine.startsWith("#!"); + const standardShebangs = ["#!/bin/sh", "#!/bin/bash", "#!/usr/bin/env bash", "#!/usr/bin/env sh"]; + const isStandard = standardShebangs.some((s) => firstLine.startsWith(s)); + return { + hasShebang, + shebangLine: hasShebang ? firstLine : undefined, + isStandard, + }; + }, +}); + +// Agent role: Validate shebang lines in all shell scripts found in the workspace. +const shellShebangValidator = agent({ + model: "small", + instructions: p`You are a shell script shebang validator. +Shell scripts found: ${p.glob("**/*.sh")} + +For each shell script, call checkShebangLine to verify the shebang. +Count files missing a shebang and those with a standard shebang. +Return the output schema.`, + output: s.object({ + files: s.record(s.object({ + hasShebang: s.boolean, + shebangLine: s.optional(s.string), + isStandard: s.boolean, + })), + missingShebangCount: s.int, + standardShebangCount: s.int, + totalFiles: s.int, + }), + tools: [checkShebangLine], + addons: [repair()], +}); + +export default shellShebangValidator; +``` diff --git a/skills/rig/samples/388-git-log-graph-summarizer.md b/skills/rig/samples/388-git-log-graph-summarizer.md new file mode 100644 index 0000000..45982bf --- /dev/null +++ b/skills/rig/samples/388-git-log-graph-summarizer.md @@ -0,0 +1,48 @@ +# 388 - Git Log Graph Summarizer + +```rig +import { agent, p, s, defineTool, steering } from "rig"; + +const parseGraphLine = defineTool("parseGraphLine", { + description: "Parse a git log --graph line and classify its type", + parameters: s.object({ + line: s.string, + }), + handler: ({ line }: { line: string }) => { + const hashMatch = line.match(/\b([0-9a-f]{7,})\b/); + const hash = hashMatch?.[1]; + const messageMatch = line.match(/[0-9a-f]{7,}\s+(.+)$/); + const message = messageMatch?.[1]; + + if (/Merge/.test(line) && hash) return { type: "merge" as const, hash, message }; + if (hash) return { type: "commit" as const, hash, message }; + if (/[|\\\/]/.test(line) && !hash) return { type: "branch-point" as const, hash: undefined, message: undefined }; + return { type: "decoration" as const, hash: undefined, message: undefined }; + }, +}); + +// Agent role: Parse and summarize a git log graph output to extract commit, merge, and branch-point counts. +const gitLogGraphSummarizer = agent({ + model: "small", + instructions: p`You are a git log graph summarizer. +Git log graph: ${p.bash("git log --oneline --graph -20 2>/dev/null || echo 'no git log available'")} + +For each line of the graph output, call parseGraphLine to classify it. +Count merges, commits, and branch-points. +Return the output schema.`, + output: s.object({ + lines: s.array(s.object({ + type: s.enum("merge", "commit", "branch-point", "decoration"), + hash: s.optional(s.string), + message: s.optional(s.string), + })), + mergeCount: s.int, + commitCount: s.int, + branchPoints: s.int, + }), + tools: [parseGraphLine], + addons: [steering()], +}); + +export default gitLogGraphSummarizer; +``` diff --git a/skills/rig/samples/389-ts-complexity-scorer.md b/skills/rig/samples/389-ts-complexity-scorer.md new file mode 100644 index 0000000..dca5ec2 --- /dev/null +++ b/skills/rig/samples/389-ts-complexity-scorer.md @@ -0,0 +1,57 @@ +# 389 - TS Complexity Scorer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const scoreFileComplexity = defineTool("scoreFileComplexity", { + description: "Score the complexity of a TypeScript file by counting nesting, ternaries, and callbacks", + parameters: s.object({ + filePath: s.path, + }), + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf-8").catch(() => ""); + const ternaries = (content.match(/\?[^?:]/g) ?? []).length; + const callbacks = (content.match(/=>\s*{/g) ?? []).length; + let maxNesting = 0; + let nesting = 0; + for (const ch of content) { + if (ch === "{") nesting++; + else if (ch === "}") nesting--; + if (nesting > maxNesting) maxNesting = nesting; + } + const score = ternaries * 1.5 + callbacks * 2 + maxNesting * 0.5; + const complexity = score > 30 ? "high" as const : score > 15 ? "medium" as const : "low" as const; + const topContributors: string[] = []; + if (callbacks > 5) topContributors.push("callbacks"); + if (ternaries > 5) topContributors.push("ternaries"); + if (maxNesting > 8) topContributors.push("deep-nesting"); + return { score, complexity, topContributors }; + }, +}); + +// Agent role: Score TypeScript files by complexity metrics and identify the most complex file. +const tsComplexityScorer = agent({ + model: "small", + instructions: p`You are a TypeScript complexity scorer. +TypeScript source files: ${p.glob("src/**/*.ts")} + +For each file, call scoreFileComplexity to get a score and complexity rating. +Compute the average score across all files. +Identify the most complex file. +Return the output schema.`, + output: s.object({ + files: s.record(s.object({ + score: s.number, + complexity: s.enum("low", "medium", "high"), + topContributors: s.array(s.string), + })), + averageScore: s.number, + mostComplexFile: s.optional(s.string), + }), + tools: [scoreFileComplexity], + addons: [repair()], +}); + +export default tsComplexityScorer; +``` diff --git a/skills/rig/samples/390-parallel-multi-tool-workflow.md b/skills/rig/samples/390-parallel-multi-tool-workflow.md new file mode 100644 index 0000000..474f691 --- /dev/null +++ b/skills/rig/samples/390-parallel-multi-tool-workflow.md @@ -0,0 +1,51 @@ +# 390 - Parallel Multi-Tool Workflow + +```rig +import { workflow, agent, p, s } from "rig"; + +// Agent role: Count files by extension in the workspace. +const fileCountAgent = agent({ + model: "small", + output: s.object({ + extCounts: s.record(s.int), + totalFiles: s.int, + topExtension: s.string, + }), + instructions: p`Count all files in the workspace by extension (excluding node_modules and .git). +Run: ${p.bash("find . -not -path '*/node_modules/*' -not -path '*/.git/*' -type f | sed 's/.*\\.//' | sort | uniq -c | sort -rn | head -20")} +Return extCounts map from extension to count, totalFiles, and topExtension.`, +}); + +// Agent role: Count environment variables by category (PATH, LOCALE, CI, HOME, CUSTOM). +const envHealthAgent = agent({ + model: "small", + output: s.object({ + categories: s.record(s.int), + totalVars: s.int, + }), + instructions: p`Analyze environment variables and group them into categories. +Run: ${p.bash("env | cut -d= -f1 | sort")} +Classify each variable name: PATH (contains PATH), LOCALE (contains LANG/LC_), CI (contains CI/GITHUB/RUNNER), HOME (HOME/USER/SHELL), or CUSTOM. +Return categories map and totalVars count.`, +}); + +// Workflow role: Run file count and env health agents in parallel, then combine into an overall health report. +const parallelMultiToolWorkflow = workflow({ + meta: { name: "workspaceHealth", description: "Parallel workspace health analysis", phases: ["Measure", "Rate"] }, + body: async ({ call, phase }) => { + phase("Measure"); + const [fileSummary, envSummary] = await Promise.all([ + call(fileCountAgent, "analyze workspace files"), + call(envHealthAgent, "analyze environment variables"), + ]); + phase("Rate"); + const overallHealth = await call.json( + `fileSummary=${JSON.stringify(fileSummary)} envSummary=${JSON.stringify(envSummary)}. Rate workspace as "healthy", "degraded", or "unknown".`, + s.enum("healthy", "degraded", "unknown"), + ); + return { fileSummary, envSummary, overallHealth }; + }, +}); + +export default parallelMultiToolWorkflow; +```