Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions skills/rig/samples/371-ts-dead-export-finder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 371 - TypeScript Dead Export Finder

```rig
import { agent, defineTool, p, s, repair, steering } from "rig";
import { readFile } from "node:fs/promises";

const findUnusedExports = defineTool("findUnusedExports", {
description: "Given a list of TS file paths and their exported symbols, return which exports are not imported in any other file",
parameters: s.object({
files: s.array(s.string("file path")),
}),
async handler({ files }) {
const exportedSymbols: Record<string, string[]> = {};
const allContent: string[] = [];
for (const f of files) {
try {
const src = await readFile(f, "utf8");
allContent.push(src);
const matches = [...src.matchAll(/^export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/gm)];
exportedSymbols[f] = matches.map((m) => m[1]);
} catch {
exportedSymbols[f] = [];
}
}
const combinedContent = allContent.join("\n");
const unusedExports: Record<string, string[]> = {};
for (const [file, symbols] of Object.entries(exportedSymbols)) {
const unused = symbols.filter((sym) => {
const importPattern = new RegExp(`import[^;]+\\b${sym}\\b`);
return !importPattern.test(combinedContent);
});
if (unused.length > 0) unusedExports[file] = unused;
}
return JSON.stringify(unusedExports);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Self-reference bug: all file contents are joined into one combinedContent string, so if a symbol appears in an import statement within the same file, it passes the check and is never flagged as unused.

💡 Suggested fix

When testing whether a symbol is imported, scan only the other files' content, not the exporting file itself:

for (const [file, symbols] of Object.entries(exportedSymbols)) {
  const otherContent = files
    .filter((f) => f !== file)
    .map((f) => allContent[files.indexOf(f)])
    .join('\n');
  const unused = symbols.filter((sym) => {
    const importPattern = new RegExp(`import[^;]+\\b${sym}\\b`);
    return !importPattern.test(otherContent);
  });
  if (unused.length > 0) unusedExports[file] = unused;
}

A symbol that is only referenced in its own file is still dead from the outside.

},
});

// Agent role: find TypeScript exported symbols that are not imported anywhere else in the project.
const deadExportFinder = agent({
model: "small",
instructions: p`You are analyzing a TypeScript project for dead exports.

Files in the project:
${p.glob("**/*.ts")}

Use the findUnusedExports tool with all discovered .ts file paths (excluding node_modules) to identify exported symbols that are never imported.

Return the results in the declared output schema.`,
output: s.object({
unusedExports: s.record(s.array(s.string)),
totalUnused: s.int,
hasDeadCode: s.boolean,
}),
tools: [findUnusedExports],
addons: [steering(), repair()],
});

export default deadExportFinder;
```
47 changes: 47 additions & 0 deletions skills/rig/samples/372-package-scripts-documenter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 372 - Package Scripts Documenter

```rig
import { agent, defineTool, p, s, repair } from "rig";

const inferScriptPurpose = defineTool("inferScriptPurpose", {
description: "Classify a npm script command into a category",
parameters: s.object({
name: s.string("script name"),
command: s.string("script command"),
}),
handler({ name, command }) {
const cmd = command.toLowerCase();
const nm = name.toLowerCase();
if (/\bbuild\b|\btsc\b|\brollup\b|\bvite build\b/.test(cmd) || nm.includes("build")) return "build" as const;
if (/\btest\b|\bvitest\b|\bjest\b|\bmocha\b/.test(cmd) || nm.includes("test")) return "test" as const;
if (/\blint\b|\beslint\b|\bprettier\b/.test(cmd) || nm.includes("lint") || nm.includes("format")) return "lint" as const;
if (/\brelease\b|\bpublish\b|\bchangeset\b/.test(cmd) || nm.includes("release")) return "release" as const;
if (/\bdev\b|\bwatch\b|\bstart\b/.test(cmd) || nm.includes("dev") || nm.includes("start")) return "dev" as const;
return "other" as const;
},
});

// Agent role: document all npm scripts from package.json into a SCRIPTS.md file.
const scriptsDocumenter = agent({
model: "small",
instructions: p`Read the project package.json:
${p.read("package.json")}

Use inferScriptPurpose to classify each script. Then write a SCRIPTS.md file documenting each script with its purpose, category, and command using ${p.write("SCRIPTS.md", "# Scripts\n\n<!-- generated by scripts-documenter -->\n")}.

Return the output schema with scripts metadata and the outputFile path.`,
output: s.object({
scripts: s.record(s.object({
purpose: s.enum("build", "test", "lint", "release", "dev", "other"),
category: s.string,
command: s.string,
})),
documentedCount: s.int,
outputFile: s.path,
}),
tools: [inferScriptPurpose],
addons: [repair()],
});

export default scriptsDocumenter;
```
44 changes: 44 additions & 0 deletions skills/rig/samples/373-git-diff-stats-summarizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 373 - Git Diff Stats Summarizer

```rig
import { agent, defineTool, p, s, repair } from "rig";

const classifyDiffEntry = defineTool("classifyDiffEntry", {
description: "Classify a diff file entry as added, modified, deleted, or renamed based on its path and stats",
parameters: s.object({
path: s.string("file path from diff"),
additions: s.int,
deletions: s.int,
}),
handler({ path, additions, deletions }) {
if (path.includes(" => ")) return "renamed" as const;
if (additions > 0 && deletions === 0) return "added" as const;
if (deletions > 0 && additions === 0) return "deleted" as const;
return "modified" as const;
},
});

// Agent role: summarize git diff stats between HEAD~1 and HEAD.
const diffStatsSummarizer = agent({
model: "small",
instructions: p`Analyze the git diff stats for the most recent commit:
${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null || git diff --numstat HEAD 2>/dev/null || echo 'no diff available'")}

For each file entry, use classifyDiffEntry to determine its classification. Return the full structured summary.`,
output: s.object({
files: s.array(s.object({
path: s.string,
additions: s.int,
deletions: s.int,
classification: s.enum("added", "modified", "deleted", "renamed"),
})),
totalAdditions: s.int,
totalDeletions: s.int,
mostChangedFile: s.optional(s.string),
}),
tools: [classifyDiffEntry],
addons: [repair()],
});

export default diffStatsSummarizer;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/374-dotenv-template-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 374 - Dotenv Template Generator

```rig
import { agent, defineTool, p, s, repair } from "rig";
import { readFile } from "node:fs/promises";

const extractEnvReferences = defineTool("extractEnvReferences", {
description: "Extract all process.env.VARNAME references from a TypeScript file",
parameters: s.object({ filePath: s.string("path to TypeScript file") }),
async handler({ filePath }) {
try {
const src = await readFile(filePath, "utf8");
const matches = [...src.matchAll(/process\.env\.([A-Z_][A-Z0-9_]*)/g)];
return [...new Set(matches.map((m) => m[1]))].join(",");
} catch {
return "";
}
},
});

// Agent role: generate a .env.template file from process.env references found in source files.
const dotenvTemplateGenerator = agent({
model: "small",
instructions: p`Generate a .env.template for this project.

Existing .env (if any):
${p.readOptional(".env", "# no .env found")}

TypeScript source files:
${p.glob("src/**/*.ts")}

For each TypeScript file path listed above, call extractEnvReferences to find process.env.VAR_NAME references.
Collect all unique variable names, compare with those already in .env, identify undocumented ones.
Then write a .env.template file using ${p.write(".env.template", "# Environment variables\n")}.

Return the output schema with templatePath, envKeys, undocumentedKeys, and templateGenerated.`,
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;
```
57 changes: 57 additions & 0 deletions skills/rig/samples/375-ts-class-hierarchy-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 375 - TypeScript Class Hierarchy Extractor

```rig
import { agent, defineTool, p, s, steering } from "rig";
import { readFile } from "node:fs/promises";

const extractClassInfo = defineTool("extractClassInfo", {
description: "Extract class declarations with extends/implements from a TypeScript file",
parameters: s.object({ filePath: s.string("path to TypeScript file") }),
async handler({ filePath }) {
try {
const src = await readFile(filePath, "utf8");
const classPattern = /(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?/g;
const results: Array<{ name: string; parent: string | null; interfaces: string[]; isAbstract: boolean }> = [];
for (const m of src.matchAll(classPattern)) {
results.push({
name: m[1],
parent: m[2] ?? null,
interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()).filter(Boolean) : [],
isAbstract: src.slice(Math.max(0, m.index! - 10), m.index!).includes("abstract"),
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] The isAbstract detection is redundant and fragile. The regex already optionally captures abstract\s+ as part of the class pattern, so m[0] (the full match) reliably includes it. The current approach slices raw source backwards by a fixed 10-character offset, which can misfire when there's whitespace or a comment between abstract and class.

💡 Suggested fix

Use the match itself:

results.push({
  name: m[1],
  parent: m[2] ?? null,
  interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()).filter(Boolean) : [],
  isAbstract: m[0].startsWith("abstract"),
});

This is unambiguous and does not require the unsafe index arithmetic.

}
return JSON.stringify(results);
} catch {
return "[]";
}
},
});

// Agent role: build a class hierarchy map from all TypeScript files in the project.
const classHierarchyExtractor = agent({
model: "small",
maxTurns: 6,
instructions: p`Extract the class hierarchy from this TypeScript project.

TypeScript files found:
${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -50")}

For each file, use extractClassInfo to find class declarations. Collect all class info, build a hierarchy by computing inheritance depth (root classes = depth 0, subclasses = parent depth + 1).

Return the complete class hierarchy in 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 classHierarchyExtractor;
```
46 changes: 46 additions & 0 deletions skills/rig/samples/376-git-bisect-helper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 376 - Git Bisect Helper

```rig
import { agent, defineTool, p, s, steering } from "rig";

const selectMidpoint = defineTool("selectMidpoint", {
description: "Select the midpoint commit from a range of commits for binary search",
parameters: s.object({
commits: s.array(s.string("commit hash oneline")),
}),
handler({ commits }) {
if (commits.length === 0) return JSON.stringify({ hash: null, index: -1 });
const mid = Math.floor(commits.length / 2);
const line = commits[mid];
const hash = line.split(" ")[0];
return JSON.stringify({ hash, index: mid, total: commits.length });
},
});

// Agent role: perform a simulated git bisect to identify a suspect bad commit.
const gitBisectHelper = agent({
model: "small",
maxTurns: 8,
instructions: p`Perform a git bisect analysis on the recent commit history.

Recent commits:
${p.bash("git log --oneline -20 2>/dev/null || echo 'no git history available'")}

Use selectMidpoint to perform binary search steps over the commit list. Simulate a bisect by selecting midpoints to narrow down the suspect commit range. After enough steps (3-4), pick the most suspect commit based on the bisect pattern.

Return the results in the output schema.`,
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],
addons: [steering()],
});

export default gitBisectHelper;
```
60 changes: 60 additions & 0 deletions skills/rig/samples/377-yaml-key-presence-validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 377 - YAML Key Presence Validator

```rig
import { agent, defineTool, p, s, repair } from "rig";

const checkYamlKey = defineTool("checkYamlKey", {
description: "Check whether a dot-notation key path exists in YAML content",
parameters: s.object({
content: s.string("YAML file content"),
keyPath: s.string("dot-notation key path like 'server.port'"),
}),
handler({ content, keyPath }) {
const parts = keyPath.split(".");
let present = false;
// Check if all parts of the key path appear in order as indented keys
let remaining = content;
for (const part of parts) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] The key-path traversal is structurally unsound: it searches for each dot-notation segment as a regex anywhere in the remaining content, not within the correct indentation scope. For example, server.port would match if port appears in a completely different section that happens to follow the server: line.

💡 Suggested approach

Either use a YAML parser (e.g., js-yaml) for correctness, or document the limitation clearly so sample users know the tool is a heuristic approximation. The current code is a valid teaching example of progressive narrowing, but the comment (line 17) should warn:

// Heuristic: searches for each key segment in remaining content.
// Does not track indentation scope — may produce false positives
// for identical key names in sibling sections.

If the sample is meant to demonstrate a real-world pattern, prefer a proper YAML parse.

const pattern = new RegExp(`(?:^|\\n)\\s*${part}\\s*:`, "m");
if (pattern.test(remaining)) {
const idx = remaining.search(pattern);
remaining = remaining.slice(idx);
present = true;
} else {
present = false;
break;
}
}
return present ? "present" : "missing";
},
});

// Agent role: validate that required keys are present in a YAML configuration file.
const yamlKeyValidator = agent({
model: "small",
input: s.object({
yamlFile: s.path,
requiredKeys: s.array(s.string),
}),
instructions: p`Validate a YAML file for required key presence.

YAML file content:
${p.readInput("yamlFile")}

Required keys to check: use the input.requiredKeys array.

For each required key, call checkYamlKey with the full file content and the key path. Collect which keys are present and which are missing.

Return the output schema with missingKeys, presentKeys, allPresent, and checkedKeys.`,
output: s.object({
missingKeys: s.array(s.string),
presentKeys: s.array(s.string),
allPresent: s.boolean,
checkedKeys: s.int,
}),
tools: [checkYamlKey],
addons: [repair()],
});

export default yamlKeyValidator;
```
Loading
Loading