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
57 changes: 57 additions & 0 deletions skills/rig/samples/361-ts-interface-inheritance-graph-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 361 - TS Interface Inheritance Graph Builder

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

const extractInterfaceExtends = defineTool("extractInterfaceExtends", {
description: "Parse a TypeScript file and return all interface declarations with their extends clauses.",
parameters: { filePath: s.path },
handler: async ({ filePath }: { filePath: string }) => {
try {
const content = await readFile(filePath, "utf-8");
const results: Array<{ name: string; parents: string[] }> = [];
const re = /interface\s+(\w+)(?:\s+extends\s+([\w,\s]+))?/g;
let m: RegExpExecArray | null;
while ((m = re.exec(content)) !== null) {
const parents = m[2]
? m[2].split(",").map((p) => p.trim()).filter(Boolean)
: [];
results.push({ name: m[1], parents });
}
return results;
} catch {
return [];
}
},
});

const tsInterfaceInheritanceGraphBuilder = agent({
model: "small",
instructions: p`Build a TypeScript interface inheritance graph.

TypeScript files:
${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -20")}

Steps:
1. For each file, call extractInterfaceExtends to get interface declarations and their parents.
2. Aggregate all interfaces into a record keyed by interface name. Each entry: parents (array of parent interface names), depth classification.
3. Classify depth: "root" = no parents, "derived" = has parents and at least one child, "leaf" = has parents but no children.
4. totalInterfaces = total count across all files.
5. maxDepth = maximum inheritance chain depth (BFS from each root).`,
output: s.object({
interfaces: s.record(
s.object({
parents: s.array(s.string),
depth: s.enum("root", "derived", "leaf"),
})
),
totalInterfaces: s.number,
maxDepth: s.number,
}),
tools: [extractInterfaceExtends],
addons: [repair()],
});

export default tsInterfaceInheritanceGraphBuilder;
```
60 changes: 60 additions & 0 deletions skills/rig/samples/362-dep-license-compatibility-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 362 - Dep License Compatibility Matrix

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

const extractLicense = defineTool("extractLicense", {
description: "Read the LICENSE file for an npm package and classify its SPDX license identifier.",
parameters: { packageName: s.string },
handler: async ({ packageName }: { packageName: string }) => {
const { execSync } = await import("node:child_process");
try {
const license = execSync(
`node -e "const p=require('./node_modules/${packageName}/package.json');console.log(p.license||'')" 2>/dev/null`,
{ encoding: "utf-8" }
).trim();

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.

[/grill-with-docs] Shell injection risk: packageName is interpolated directly into an execSync shell string — a crafted package name (e.g. containing ; or $()) could execute arbitrary commands.

💡 Suggested fix

Read the package.json directly instead of using execSync:

const { readFile } = await import("node:fs/promises");
const pkg = JSON.parse(await readFile(`./node_modules/${packageName}/package.json`, "utf-8"));
const license = pkg.license ?? "";

Or at minimum validate packageName against a safe identifier pattern before interpolating.

const permissive = ["MIT", "ISC", "BSD-2-Clause", "BSD-3-Clause", "Apache-2.0", "0BSD", "Unlicense"];
const copyleft = ["GPL-2.0", "GPL-3.0", "LGPL-2.1", "LGPL-3.0", "AGPL-3.0", "MPL-2.0"];
if (!license) return { spdxId: "unknown", compatibility: "unknown" as const };
if (permissive.some((l) => license.includes(l))) return { spdxId: license, compatibility: "permissive" as const };
if (copyleft.some((l) => license.includes(l))) return { spdxId: license, compatibility: "copyleft" as const };
if (license.toLowerCase().includes("proprietary") || license.toLowerCase().includes("commercial"))
return { spdxId: license, compatibility: "proprietary" as const };
return { spdxId: license, compatibility: "unknown" as const };
} catch {
return { spdxId: "unknown", compatibility: "unknown" as const };
}
},
});

const depLicenseCompatibilityMatrix = agent({
model: "small",
instructions: p`Build a dependency license compatibility matrix.

package.json contents:
${p.read("package.json")}

Steps:
1. Parse the dependencies and devDependencies from package.json.
2. For each package name, call extractLicense to get its spdxId and compatibility.
3. Build a packages record keyed by package name with spdxId and compatibility.
4. copyleftCount = number of packages classified as "copyleft".
5. unknownCount = number classified as "unknown".
6. hasConflicts = true if copyleftCount > 0.`,
output: s.object({
packages: s.record(
s.object({
spdxId: s.string,
compatibility: s.enum("permissive", "copyleft", "proprietary", "unknown"),
})
),
copyleftCount: s.number,
unknownCount: s.number,
hasConflicts: s.boolean,
}),
tools: [extractLicense],
addons: [repair()],
});

export default depLicenseCompatibilityMatrix;
```
56 changes: 56 additions & 0 deletions skills/rig/samples/363-tsconfig-options-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 363 - Tsconfig Options Analyzer

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

const parseTsConfigOption = defineTool("parseTsConfigOption", {
description: "Classify a TypeScript compiler option into a category.",
parameters: { key: s.string, value: s.unknown },
handler: ({ key }: { key: string; value: unknown }) => {
const strict = ["strict", "strictNullChecks", "strictFunctionTypes", "strictBindCallApply", "strictPropertyInitialization", "noImplicitAny", "noImplicitThis", "alwaysStrict"];
const perf = ["skipLibCheck", "skipDefaultLibCheck", "incremental", "tsBuildInfoFile"];
const output = ["outDir", "outFile", "declaration", "declarationDir", "declarationMap", "sourceMap", "removeComments", "noEmit", "emitDeclarationOnly"];
const paths = ["baseUrl", "paths", "rootDir", "rootDirs", "typeRoots", "types"];
if (strict.includes(key)) return { category: "strict" as const, recommended: true };
if (perf.includes(key)) return { category: "perf" as const, recommended: false };
if (output.includes(key)) return { category: "output" as const, recommended: false };
if (paths.includes(key)) return { category: "paths" as const, recommended: false };
return { category: "misc" as const, recommended: false };
},
});

const tsconfigOptionsAnalyzer = agent({
model: "small",
instructions: p`Analyze TypeScript compiler options across tsconfig files.

Main tsconfig.json:
${p.read("tsconfig.json")}

Variant tsconfig files found:
${p.bash("ls tsconfig.*.json 2>/dev/null || echo 'none'")}

Steps:
1. Parse compilerOptions from the main tsconfig.json.
2. For each compiler option key/value, call parseTsConfigOption to get category and recommended.
3. Build an options record keyed by option name with value, category, recommended.
4. strictCount = number of options with category "strict".
5. hasIsolatedModules = true if "isolatedModules" option is present and set to true.
6. configFilesFound = list of tsconfig files found (include tsconfig.json and any variants).`,
output: s.object({
options: s.record(
s.object({
value: s.unknown,
category: s.enum("strict", "perf", "output", "paths", "misc"),
recommended: s.boolean,
})
),
strictCount: s.number,
hasIsolatedModules: s.boolean,
configFilesFound: s.array(s.string),
}),
tools: [parseTsConfigOption],
addons: [repair()],
});

export default tsconfigOptionsAnalyzer;
```
51 changes: 51 additions & 0 deletions skills/rig/samples/364-git-patch-format-summarizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 364 - Git Patch Format Summarizer

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

const extractPatchMetadata = defineTool("extractPatchMetadata", {
description: "Parse a git patch block and extract author, subject, files changed, insertions, and deletions.",
parameters: { patch: s.string },
handler: ({ patch }: { patch: string }) => {
const author = (patch.match(/^From: (.+)$/m) ?? [])[1]?.trim() ?? "unknown";
const subject = (patch.match(/^Subject: (.+)$/m) ?? [])[1]?.replace(/^\[PATCH[^\]]*\] /, "").trim() ?? "unknown";
const filesChanged = parseInt((patch.match(/(\d+) file[s]? changed/) ?? [])[1] ?? "0", 10);
const insertions = parseInt((patch.match(/(\d+) insertion/) ?? [])[1] ?? "0", 10);
const deletions = parseInt((patch.match(/(\d+) deletion/) ?? [])[1] ?? "0", 10);
return { author, subject, filesChanged, insertions, deletions };
},
});

const gitPatchFormatSummarizer = agent({
model: "small",
instructions: p`Summarize the last 5 git commits as patch metadata.

Patch output:
${p.bash("git format-patch -5 --stdout 2>/dev/null | head -500 || echo 'no patches'")}

Steps:
1. Split the patch output into individual patch blocks (each starts with "From <hash>").
2. For each patch block, call extractPatchMetadata to get author, subject, filesChanged, insertions, deletions.
3. Build patches array from results.
4. totalPatches = patches.length.
5. topContributor = author name that appears most frequently (omit if no patches).`,
output: s.object({
patches: s.array(
s.object({
author: s.string,
subject: s.string,
filesChanged: s.number,
insertions: s.number,
deletions: s.number,
})
),
totalPatches: s.number,
topContributor: s.optional(s.string),
}),
tools: [extractPatchMetadata],
addons: [steering()],
});

export default gitPatchFormatSummarizer;
```
59 changes: 59 additions & 0 deletions skills/rig/samples/365-npm-lifecycle-script-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 365 - NPM Lifecycle Script Analyzer

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

const classifyScript = defineTool("classifyScript", {
description: "Classify an npm script by its lifecycle category.",
parameters: { name: s.string, command: s.string },
handler: ({ name, command }: { name: string; command: string }) => {
const hookPrefixes = ["pre", "post"];
const isHook = hookPrefixes.some((prefix) =>
["build", "test", "install", "pack", "publish", "start", "stop", "restart"].some(
(base) => name === `${prefix}${base}`
)
);
let category: "build" | "test" | "lint" | "release" | "hook" | "other" = "other";
if (isHook) category = "hook";
else if (/\b(build|compile|tsc|webpack|rollup|vite|esbuild)\b/.test(command)) category = "build";
else if (/\b(test|jest|vitest|mocha|jasmine|tap)\b/.test(command)) category = "test";
else if (/\b(lint|eslint|tslint|prettier|stylelint|biome)\b/.test(command)) category = "lint";
else if (/\b(release|publish|deploy|version|changelog)\b/.test(command)) category = "release";
return { category, isHook };
},
});

const npmLifecycleScriptAnalyzer = agent({
model: "small",
instructions: p`Analyze npm lifecycle scripts from package.json.

package.json:
${p.read("package.json")}

Steps:
1. Parse the scripts field from package.json.
2. For each script entry (name, command), call classifyScript to get category and isHook.
3. Build scripts record keyed by script name with command, category, isHook.
4. hookCount = number of scripts with isHook true.
5. missingRecommended = array of recommended script names not present: ["test", "build", "lint"].
6. hasTestScript = "test" key exists in scripts.
7. hasBuildScript = "build" key exists in scripts.`,
output: s.object({
scripts: s.record(
s.object({
command: s.string,
category: s.enum("build", "test", "lint", "release", "hook", "other"),
isHook: s.boolean,
})
),
hookCount: s.number,
missingRecommended: s.array(s.string),
hasTestScript: s.boolean,
hasBuildScript: s.boolean,
}),
tools: [classifyScript],
addons: [repair()],
});

export default npmLifecycleScriptAnalyzer;
```
69 changes: 69 additions & 0 deletions skills/rig/samples/366-ts-jsdoc-coverage-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 366 - TS JSDoc Coverage Checker

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

const analyzeFunctionComments = defineTool("analyzeFunctionComments", {
description: "Scan a TypeScript file for exported functions and count those with and without JSDoc comments.",
parameters: { filePath: s.path },
handler: async ({ filePath }: { filePath: string }) => {
try {
const content = await readFile(filePath, "utf-8");
const lines = content.split("\n");
let documentedCount = 0;
let undocumentedCount = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (/^export\s+(async\s+)?function\s+\w+/.test(line) || /^export\s+const\s+\w+\s*=\s*(async\s*)?\(/.test(line)) {
const prevLine = lines[i - 1]?.trim() ?? "";
const prevPrevLine = lines[i - 2]?.trim() ?? "";
if (prevLine.endsWith("*/") || prevPrevLine.endsWith("*/") || prevLine.startsWith("/**") || prevLine.startsWith("*")) {

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.

[/grill-with-docs] JSDoc detection heuristic is fragile: the tool checks the immediately previous 1–2 lines for */ or /**, but multiline JSDoc blocks have many intermediate lines. A function preceded by a 5-line JSDoc comment would be counted as undocumented because prevLine would be a * continuation line rather than */.

💡 Suggested fix

Scan backwards from i-1 until a non-* line or blank line is found:

let hasJsDoc = false;
for (let j = i - 1; j >= 0; j--) {
  const t = lines[j].trim();
  if (t.endsWith("*/") || t.startsWith("/**")) { hasJsDoc = true; break; }
  if (t === "" || (!t.startsWith("*") && !t.startsWith("/"))) break;
}
if (hasJsDoc) documentedCount++; else undocumentedCount++;

documentedCount++;
} else {
undocumentedCount++;
}
}
}
const total = documentedCount + undocumentedCount;
const coverage = total > 0 ? documentedCount / total : 1;
return { documentedCount, undocumentedCount, coverage };
} catch {
return { documentedCount: 0, undocumentedCount: 0, coverage: 0 };
}
},
});

const tsJsDocCoverageChecker = agent({
model: "small",
instructions: p`Check JSDoc coverage for exported TypeScript functions.

TypeScript files:
${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -20")}

Steps:
1. For each file path, call analyzeFunctionComments to get documentedCount, undocumentedCount, and coverage.
2. Build files record keyed by file path with those three values.
3. overall: totalFunctions = sum of all (documentedCount + undocumentedCount), documentedFunctions = sum of all documentedCount, coveragePercent = (documentedFunctions / totalFunctions) * 100 (or 100 if no functions).
4. wellDocumentedFiles = file paths where coverage >= 0.8.`,
output: s.object({
files: s.record(
s.object({
documentedCount: s.number,
undocumentedCount: s.number,
coverage: s.number,
})
),
overall: s.object({
totalFunctions: s.number,
documentedFunctions: s.number,
coveragePercent: s.number,
}),
wellDocumentedFiles: s.array(s.string),
}),
tools: [analyzeFunctionComments],
addons: [repair()],
});

export default tsJsDocCoverageChecker;
```
Loading
Loading