-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-16 #434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, unknown>)) { | ||
| 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; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, { methodCount: number; hasOptionalMethods: boolean; sourceFile: string }> = {}; | ||
| 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) ?? []); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] The regex 💡 FixFor a sample, acknowledging the limitation in a comment is fine. If correctness matters, consider a brace-counting approach: // Simple depth-aware body extraction
function extractInterfaceBody(src: string, start: number): string {
let depth = 0, i = start;
while (i < src.length) {
if (src[i] === '{') { depth++; }
else if (src[i] === '}') { if (--depth === 0) return src.slice(start + 1, i); }
i++;
}
return "";
}Or note the limitation with a comment so readers understand the scope. |
||
| 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; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] Same shell injection pattern as 423: 💡 FixUse import { spawnSync } from "node:child_process";
const typeResult = spawnSync("git", ["cat-file", "-t", `refs/tags/${tagName}`], { encoding: "utf-8" });
const objType = typeResult.status === 0 ? typeResult.stdout.trim() : "lightweight"; |
||
| 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); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] The 💡 FixParse the epoch integer and convert it: const dateMatch = body.match(/^tagger .+ (\d{10,})/m);
const date = dateMatch ? new Date(parseInt(dateMatch[1], 10) * 1000).toISOString().slice(0, 10) : ""; |
||
| 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; | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/grill-with-docs] Shell injection risk:
revisionandfilePathare interpolated directly into a shell command withexecSync. If the agent passes paths containing spaces, quotes, or;, the command will misbehave or execute unintended commands.💡 Fix
Use
spawnSyncto pass arguments as an array, bypassing the shell entirely: