diff --git a/skills/rig/samples/421-html-anchor-extractor.md b/skills/rig/samples/421-html-anchor-extractor.md new file mode 100644 index 0000000..34264ac --- /dev/null +++ b/skills/rig/samples/421-html-anchor-extractor.md @@ -0,0 +1,61 @@ +# 421 - Html Anchor Extractor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const extractAnchors = defineTool("extractAnchors", { + description: "Extract all anchor href values from an HTML file and classify each link.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + try { + const content = await readFile(filePath, "utf-8"); + const re = /href=["']([^"']+)["']/gi; + const results: Array<{ url: string; type: "internal" | "external" | "fragment"; file: string }> = []; + let m: RegExpExecArray | null; + while ((m = re.exec(content)) !== null) { + const url = m[1]; + let type: "internal" | "external" | "fragment" = "internal"; + if (url.startsWith("#")) type = "fragment"; + else if (/^https?:\/\//.test(url) || url.startsWith("//")) type = "external"; + results.push({ url, type, file: filePath }); + } + return results; + } catch { + return []; + } + }, +}); + +// Agent role: extract and classify all anchor links from HTML files in the workspace. +const htmlAnchorExtractor = agent({ + model: "small", + instructions: p`Extract and classify all anchor href links from HTML files. + +HTML files in workspace: +${p.bash("find . -name '*.html' -not -path '*/node_modules/*' | head -30")} + +Steps: +1. For each file path, call extractAnchors to get links array. +2. Combine all links into a single links array. +3. totalLinks = links.length. +4. externalCount = links where type === "external". +5. internalCount = links where type === "internal". +6. fragmentCount = links where type === "fragment".`, + output: s.object({ + links: s.array(s.object({ + url: s.string, + type: s.enum("internal", "external", "fragment"), + file: s.string, + })), + totalLinks: s.int, + externalCount: s.int, + internalCount: s.int, + fragmentCount: s.int, + }), + tools: [extractAnchors], + addons: [repair()], +}); + +export default htmlAnchorExtractor; +``` diff --git a/skills/rig/samples/422-ts-optional-chaining-counter.md b/skills/rig/samples/422-ts-optional-chaining-counter.md new file mode 100644 index 0000000..f79e7de --- /dev/null +++ b/skills/rig/samples/422-ts-optional-chaining-counter.md @@ -0,0 +1,51 @@ +# 422 - Ts Optional Chaining Counter + +```rig +import { agent, p, s, defineTool, steering } from "rig"; +import { readFile } from "node:fs/promises"; + +const countNullSafetyOperators = defineTool("countNullSafetyOperators", { + description: "Count optional chaining (?.) and nullish coalescing (??) operators in a TypeScript file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + try { + const content = await readFile(filePath, "utf-8"); + const optionalChainingCount = (content.match(/\?\./g) ?? []).length; + const nullishCoalescingCount = (content.match(/\?\?(?!=)/g) ?? []).length; + return { optionalChainingCount, nullishCoalescingCount, total: optionalChainingCount + nullishCoalescingCount }; + } catch { + return { optionalChainingCount: 0, nullishCoalescingCount: 0, total: 0 }; + } + }, +}); + +// Agent role: count optional chaining and nullish coalescing operators across TypeScript files. +const tsOptionalChainingCounter = agent({ + model: "small", + instructions: p`Count optional chaining (?.) and nullish coalescing (??) operator usage in TypeScript files. + +TypeScript files found: +${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -name '*.d.ts' | head -25")} + +Steps: +1. For each file path, call countNullSafetyOperators to get per-file counts. +2. Build files record keyed by file path. +3. totalOptionalChaining = sum of all optionalChainingCount. +4. totalNullishCoalescing = sum of all nullishCoalescingCount. +5. mostUsedFile = path with highest total (omit if all totals are 0).`, + output: s.object({ + files: s.record(s.object({ + optionalChainingCount: s.int, + nullishCoalescingCount: s.int, + total: s.int, + })), + totalOptionalChaining: s.int, + totalNullishCoalescing: s.int, + mostUsedFile: s.optional(s.string), + }), + tools: [countNullSafetyOperators], + addons: [steering()], +}); + +export default tsOptionalChainingCounter; +``` diff --git a/skills/rig/samples/423-git-file-size-tracker.md b/skills/rig/samples/423-git-file-size-tracker.md new file mode 100644 index 0000000..dadda76 --- /dev/null +++ b/skills/rig/samples/423-git-file-size-tracker.md @@ -0,0 +1,54 @@ +# 423 - Git File Size Tracker + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { execSync } from "node:child_process"; + +const getFileSizeAtRevision = defineTool("getFileSizeAtRevision", { + description: "Get the byte size of a file at a specific git revision using git cat-file.", + parameters: s.object({ revision: s.string, filePath: s.string }), + handler({ revision, filePath }: { revision: string; filePath: string }) { + try { + const out = execSync(`git cat-file -s "${revision}:${filePath}" 2>/dev/null || echo 0`, { encoding: "utf-8" }); + const size = parseInt(out.trim(), 10); + return { size: isNaN(size) ? 0 : size }; + } catch { + return { size: 0 }; + } + }, +}); + +// Agent role: track file byte-size changes between HEAD~1 and HEAD. +const gitFileSizeTracker = agent({ + model: "small", + instructions: p`Track file size changes between the previous commit (HEAD~1) and current HEAD. + +Files changed in the last commit: +${p.bash("git diff --name-only HEAD~1 HEAD 2>/dev/null || echo ''")} + +Steps: +1. For each file path, call getFileSizeAtRevision with "HEAD~1" then "HEAD". +2. delta = currentSize - previousSize. +3. change: "grew" if delta > 0, "shrank" if delta < 0, "unchanged" if 0. +4. Build files array with path, previousSize, currentSize, delta, change. +5. totalFiles = files.length. +6. largestGrowth = path with highest positive delta (omit if none grew). +7. largestShrink = path with most negative delta (omit if none shrank).`, + output: s.object({ + files: s.array(s.object({ + path: s.string, + previousSize: s.int, + currentSize: s.int, + delta: s.int, + change: s.enum("grew", "shrank", "unchanged"), + })), + totalFiles: s.int, + largestGrowth: s.optional(s.string), + largestShrink: s.optional(s.string), + }), + tools: [getFileSizeAtRevision], + addons: [repair()], +}); + +export default gitFileSizeTracker; +``` diff --git a/skills/rig/samples/424-json-pretty-printer.md b/skills/rig/samples/424-json-pretty-printer.md new file mode 100644 index 0000000..4e42339 --- /dev/null +++ b/skills/rig/samples/424-json-pretty-printer.md @@ -0,0 +1,75 @@ +# 424 - Json Pretty Printer + +```rig +import { agent, s, defineTool, repair } from "rig"; +import { readFile, writeFile } from "node:fs/promises"; + +const readJsonFile = defineTool("readJsonFile", { + description: "Read and parse a JSON file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + const content = await readFile(filePath, "utf-8"); + return JSON.parse(content) as unknown; + }, +}); + +const analyzeJsonStructure = defineTool("analyzeJsonStructure", { + description: "Recursively count keys, depth, arrays, and objects in a JSON value.", + parameters: s.object({ json: s.unknown }), + handler({ json }: { json: unknown }) { + let keyCount = 0; + let depth = 0; + let arrayCount = 0; + let objectCount = 0; + function walk(val: unknown, d: number): void { + if (d > depth) depth = d; + if (Array.isArray(val)) { + arrayCount++; + for (const item of val) walk(item, d + 1); + } else if (val !== null && typeof val === "object") { + objectCount++; + for (const v of Object.values(val as Record)) { + keyCount++; + walk(v, d + 1); + } + } + } + walk(json, 0); + return { keyCount, depth, arrayCount, objectCount }; + }, +}); + +const writeJsonFile = defineTool("writeJsonFile", { + description: "Write a value as pretty-printed JSON to a file.", + parameters: s.object({ filePath: s.path, content: s.unknown }), + async handler({ filePath, content }: { filePath: string; content: unknown }) { + await writeFile(filePath, JSON.stringify(content, null, 2), "utf-8"); + return { written: true }; + }, +}); + +// Agent role: pretty-print a JSON file and report structural statistics. +const jsonPrettyPrinter = agent({ + model: "small", + input: s.object({ inputFile: s.string, outputFile: s.string }), + instructions: `Pretty-print a JSON file and return structural statistics. + +Steps: +1. Call readJsonFile with inputFile from input. +2. Call analyzeJsonStructure with the parsed JSON to get keyCount, depth, arrayCount, objectCount. +3. Call writeJsonFile with outputFile and the parsed JSON. +4. Return all stats, outputFile from input, and prettyPrinted: true.`, + output: s.object({ + keyCount: s.int, + depth: s.int, + arrayCount: s.int, + objectCount: s.int, + outputFile: s.string, + prettyPrinted: s.boolean, + }), + tools: [readJsonFile, analyzeJsonStructure, writeJsonFile], + addons: [repair()], +}); + +export default jsonPrettyPrinter; +``` diff --git a/skills/rig/samples/425-ts-interface-method-counter.md b/skills/rig/samples/425-ts-interface-method-counter.md new file mode 100644 index 0000000..c39b82e --- /dev/null +++ b/skills/rig/samples/425-ts-interface-method-counter.md @@ -0,0 +1,55 @@ +# 425 - Ts Interface Method Counter + +```rig +import { agent, p, s, defineTool, steering } from "rig"; +import { readFile } from "node:fs/promises"; + +const countInterfaceMethods = defineTool("countInterfaceMethods", { + description: "Count method signatures inside TypeScript interface declarations in a file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + const content = await readFile(filePath, "utf-8"); + const results: Record = {}; + const ifaceRe = /interface\s+(\w+)[^{]*\{([^}]*)\}/gs; + let m: RegExpExecArray | null; + while ((m = ifaceRe.exec(content)) !== null) { + const name = m[1]; + const body = m[2]; + const methods = (body.match(/\w+\??\s*\([^)]*\)/g) ?? []); + const hasOptionalMethods = /\w+\?\s*\(/.test(body); + results[name] = { methodCount: methods.length, hasOptionalMethods, sourceFile: filePath }; + } + return results; + }, +}); + +// Agent role: count method signatures in TypeScript interfaces across source files. +const tsInterfaceMethodCounter = agent({ + model: "small", + instructions: p`Count method signatures in TypeScript interface declarations. + +TypeScript source files: +${p.glob("src/**/*.ts")} + +For each file path, call countInterfaceMethods and merge the returned records. +Compute: +- totalInterfaces = total interface names found +- averageMethodCount = total methods / totalInterfaces (0 if none) +- largestInterface = name with most methods (omit if no interfaces)`, + 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), + }), + tools: [countInterfaceMethods], + maxTurns: 6, + addons: [steering()], +}); + +export default tsInterfaceMethodCounter; +``` diff --git a/skills/rig/samples/426-git-tag-annotation-extractor.md b/skills/rig/samples/426-git-tag-annotation-extractor.md new file mode 100644 index 0000000..eea28dc --- /dev/null +++ b/skills/rig/samples/426-git-tag-annotation-extractor.md @@ -0,0 +1,57 @@ +# 426 - Git Tag Annotation Extractor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { execSync } from "node:child_process"; + +const classifyTagType = defineTool("classifyTagType", { + description: "Classify a git tag as annotated, lightweight, or signed.", + parameters: s.object({ tagName: s.string }), + handler({ tagName }: { tagName: string }) { + try { + const objType = execSync(`git cat-file -t "refs/tags/${tagName}" 2>/dev/null || echo lightweight`, { encoding: "utf-8" }).trim(); + if (objType === "tag") { + const body = execSync(`git cat-file tag "refs/tags/${tagName}" 2>/dev/null || echo ""`, { encoding: "utf-8" }); + if (/BEGIN PGP/.test(body)) return { type: "signed" as const, subject: "", date: "" }; + const subjectMatch = body.match(/^subject (.+)$/m); + const dateMatch = body.match(/^tagger .+ (\d{4}-\d{2}-\d{2})/m); + return { + type: "annotated" as const, + subject: subjectMatch ? subjectMatch[1] : "", + date: dateMatch ? dateMatch[1] : "", + }; + } + return { type: "lightweight" as const, subject: "", date: "" }; + } catch { + return { type: "lightweight" as const, subject: "", date: "" }; + } + }, +}); + +// Agent role: extract and classify all git tag annotations in the repository. +const gitTagAnnotationExtractor = agent({ + model: "small", + instructions: p`Extract and classify git tag annotations. + +All tags in repository: +${p.bash("git tag -l | head -50")} + +For each tag name, call classifyTagType to determine type, subject, and date. +Build tags record keyed by tag name. +Count annotatedCount, lightweightCount, totalTags.`, + 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, + }), + tools: [classifyTagType], + addons: [repair()], +}); + +export default gitTagAnnotationExtractor; +``` diff --git a/skills/rig/samples/427-ts-wildcard-reexport-detector.md b/skills/rig/samples/427-ts-wildcard-reexport-detector.md new file mode 100644 index 0000000..5721cd4 --- /dev/null +++ b/skills/rig/samples/427-ts-wildcard-reexport-detector.md @@ -0,0 +1,55 @@ +# 427 - Ts Wildcard Reexport Detector + +```rig +import { agent, p, s, defineTool, steering } from "rig"; +import { readFile } from "node:fs/promises"; + +const scanWildcardReexports = defineTool("scanWildcardReexports", { + description: "Scan a TypeScript file for 'export * from' wildcard re-export patterns.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + try { + const content = await readFile(filePath, "utf-8"); + const re = /export\s+\*(?:\s+as\s+\w+)?\s+from\s+["']([^"']+)["']/g; + const targets: string[] = []; + let m: RegExpExecArray | null; + while ((m = re.exec(content)) !== null) { + targets.push(m[1]); + } + const hasNamespaceExport = /export\s+\*\s+as\s+\w+/.test(content); + return { reexportCount: targets.length, targets, hasNamespaceExport }; + } catch { + return { reexportCount: 0, targets: [], hasNamespaceExport: false }; + } + }, +}); + +// Agent role: detect wildcard re-export patterns across TypeScript source files. +const tsWildcardReexportDetector = agent({ + model: "small", + instructions: p`Detect wildcard re-export patterns (export * from ...) in TypeScript files. + +TypeScript source files: +${p.glob("src/**/*.ts")} + +For each file path, call scanWildcardReexports. +Build files record keyed by file path. +totalReexports = sum of all reexportCount. +totalFiles = number of files processed. +mostReexportedFile = file with highest reexportCount (omit if all are 0).`, + output: s.object({ + files: s.record(s.object({ + reexportCount: s.int, + targets: s.array(s.string), + hasNamespaceExport: s.boolean, + })), + totalReexports: s.int, + totalFiles: s.int, + mostReexportedFile: s.optional(s.string), + }), + tools: [scanWildcardReexports], + addons: [steering()], +}); + +export default tsWildcardReexportDetector; +``` diff --git a/skills/rig/samples/428-markdown-link-classifier.md b/skills/rig/samples/428-markdown-link-classifier.md new file mode 100644 index 0000000..e337525 --- /dev/null +++ b/skills/rig/samples/428-markdown-link-classifier.md @@ -0,0 +1,60 @@ +# 428 - Markdown Link Classifier + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const extractLinks = defineTool("extractLinks", { + description: "Extract and classify all markdown links from a file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + try { + const content = await readFile(filePath, "utf-8"); + const re = /\[([^\]]*)\]\(([^)]+)\)/g; + let linkCount = 0; + let absoluteCount = 0; + let relativeCount = 0; + let anchorCount = 0; + let m: RegExpExecArray | null; + while ((m = re.exec(content)) !== null) { + const url = m[2]; + linkCount++; + if (url.startsWith("#")) anchorCount++; + else if (/^https?:\/\//.test(url) || url.startsWith("mailto:")) absoluteCount++; + else relativeCount++; + } + return { linkCount, absoluteCount, relativeCount, anchorCount }; + } catch { + return { linkCount: 0, absoluteCount: 0, relativeCount: 0, anchorCount: 0 }; + } + }, +}); + +// Agent role: find and classify all links in markdown files across the workspace. +const markdownLinkClassifier = agent({ + model: "small", + instructions: p`Extract and classify all markdown links across .md files. + +Markdown files in workspace: +${p.glob("**/*.md")} + +For each file path, call extractLinks to get per-file link counts. +Build files record keyed by file path. +totalLinks = sum of all linkCount. +absoluteLinksCount = sum of all absoluteCount.`, + output: s.object({ + files: s.record(s.object({ + linkCount: s.int, + absoluteCount: s.int, + relativeCount: s.int, + anchorCount: s.int, + })), + totalLinks: s.int, + absoluteLinksCount: s.int, + }), + tools: [extractLinks], + addons: [repair()], +}); + +export default markdownLinkClassifier; +``` diff --git a/skills/rig/samples/429-package-json-field-auditor.md b/skills/rig/samples/429-package-json-field-auditor.md new file mode 100644 index 0000000..e78eb1c --- /dev/null +++ b/skills/rig/samples/429-package-json-field-auditor.md @@ -0,0 +1,49 @@ +# 429 - Package Json Field Auditor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const checkFieldPresence = defineTool("checkFieldPresence", { + description: "Check whether a field is present, absent, or malformed in a parsed package.json object.", + parameters: s.object({ fieldName: s.string, packageJson: s.unknown }), + handler({ fieldName, packageJson }: { fieldName: string; packageJson: unknown }) { + const pkg = packageJson as Record; + if (!(fieldName in pkg)) return { status: "absent" as const, value: undefined }; + const val = pkg[fieldName]; + if (val === null || val === undefined || val === "") return { status: "malformed" as const, value: String(val) }; + return { status: "present" as const, value: typeof val === "string" ? val : JSON.stringify(val) }; + }, +}); + +// Agent role: audit package.json for presence and completeness of standard fields. +const packageJsonFieldAuditor = agent({ + model: "small", + instructions: p`Audit package.json for presence and quality of standard fields. + +package.json contents: +${p.read("package.json")} + +Standard fields to check: name, version, description, main, types, exports, license, repository, keywords. + +For each field, call checkFieldPresence with the field name and the full parsed package.json. +Build fields record keyed by field name. +presentCount = fields where status === "present". +absentCount = fields where status === "absent". +totalChecked = 9. +completenessScore = presentCount / totalChecked (range 0.0 to 1.0).`, + output: s.object({ + fields: s.record(s.object({ + status: s.enum("present", "absent", "malformed"), + value: s.optional(s.string), + })), + presentCount: s.int, + absentCount: s.int, + totalChecked: s.int, + completenessScore: s.number, + }), + tools: [checkFieldPresence], + addons: [repair()], +}); + +export default packageJsonFieldAuditor; +``` diff --git a/skills/rig/samples/430-parallel-git-stats-workflow.md b/skills/rig/samples/430-parallel-git-stats-workflow.md new file mode 100644 index 0000000..9c5f310 --- /dev/null +++ b/skills/rig/samples/430-parallel-git-stats-workflow.md @@ -0,0 +1,79 @@ +# 430 - Parallel Git Stats Workflow + +```rig +import { workflow, agent, p, s } from "rig"; + +// Agent role: count commits per author from git log. +const commitStatsAgent = agent({ + model: "small", + output: s.object({ + authors: s.record(s.int), + totalCommits: s.int, + }), + instructions: p`Count commits per author. + +Git commit author counts: +${p.bash("git log --format='%aN' | sort | uniq -c | sort -rn | head -20")} + +Parse each line (format: " COUNT AUTHOR"). Build authors record keyed by author name. +totalCommits = sum of all counts.`, +}); + +// Agent role: count repository files by extension. +const fileStatsAgent = agent({ + model: "small", + output: s.object({ + extensions: s.record(s.int), + totalFiles: s.int, + }), + instructions: p`Count repository files by extension. + +File extension counts: +${p.bash("git ls-files | grep '\\.' | sed 's/.*\\.//' | sort | uniq -c | sort -rn | head -20")} + +Parse each line (format: " COUNT EXT"). Build extensions record keyed by extension. +totalFiles = sum of all counts.`, +}); + +// Workflow role: run commit stats and file stats in parallel, then combine results. +const parallelGitStatsWorkflow = workflow({ + meta: { name: "parallel-git-stats", description: "Run commit and file stats in parallel." }, + body: async ({ call }) => { + const [authorStats, fileStats] = await Promise.all([ + call(commitStatsAgent, "count commits by author"), + call(fileStatsAgent, "count files by extension"), + ]); + + const topContributor = authorStats + ? Object.entries(authorStats.authors).sort(([, a], [, b]) => (b as number) - (a as number))[0]?.[0] + : undefined; + const topExtension = fileStats + ? Object.entries(fileStats.extensions).sort(([, a], [, b]) => (b as number) - (a as number))[0]?.[0] + : undefined; + + const health = (!authorStats || authorStats.totalCommits === 0) + ? "empty" + : authorStats.totalCommits < 10 + ? "sparse" + : "healthy"; + + return call.json( + `Combine these git repository stats into the final output. +authorStats: ${JSON.stringify(authorStats)} +fileStats: ${JSON.stringify(fileStats)} +topContributor: ${topContributor ?? ""} +topExtension: ${topExtension ?? ""} +overallHealth: ${health}`, + s.object({ + authorStats: s.record(s.int), + fileStats: s.record(s.int), + topContributor: s.optional(s.string), + topExtension: s.optional(s.string), + overallHealth: s.enum("healthy", "sparse", "empty"), + }) + ); + }, +}); + +export default parallelGitStatsWorkflow; +```