Skip to content

Commit 5bc52e3

Browse files
Add 10 rig samples 310-319 (2026-07-30) (#294)
1 parent 394b566 commit 5bc52e3

10 files changed

Lines changed: 543 additions & 0 deletions
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# 310 - Lock File Drift Detector
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
6+
// Agent role: detect version drift between package.json dependencies and package-lock.json locked versions.
7+
const lockFileDriftDetector = agent({
8+
model: "small",
9+
instructions: p`You are a lock file drift detector.
10+
11+
Read the package files:
12+
${p.bash("cat package.json 2>/dev/null || echo '{}'")}
13+
${p.bash("cat package-lock.json 2>/dev/null || echo '{}'")}
14+
15+
Use the checkVersionDrift tool to cross-check declared vs locked versions for each dependency.
16+
Return the declared output.`,
17+
tools: [
18+
defineTool("checkVersionDrift", {
19+
description: "Parse package.json and package-lock.json content and return drifted packages",
20+
parameters: s.object({
21+
packageJsonContent: s.string,
22+
lockFileContent: s.string,
23+
}),
24+
handler({ packageJsonContent, lockFileContent }) {
25+
let pkg: Record<string, unknown>;
26+
let lock: Record<string, unknown>;
27+
try {
28+
pkg = JSON.parse(packageJsonContent) as Record<string, unknown>;
29+
} catch {
30+
return { error: "Failed to parse package.json" };
31+
}
32+
try {
33+
lock = JSON.parse(lockFileContent) as Record<string, unknown>;
34+
} catch {
35+
return { error: "Failed to parse package-lock.json" };
36+
}
37+
const deps = {
38+
...((pkg["dependencies"] as Record<string, string>) ?? {}),
39+
...((pkg["devDependencies"] as Record<string, string>) ?? {}),
40+
};
41+
const lockPackages = (lock["packages"] as Record<string, { version?: string }>) ?? {};
42+
const drifted: Array<{ name: string; declared: string; locked: string }> = [];
43+
for (const [name, declared] of Object.entries(deps)) {
44+
const lockKey = `node_modules/${name}`;
45+
const lockedEntry = lockPackages[lockKey];
46+
const locked = lockedEntry?.version ?? "missing";
47+
const declaredClean = (declared as string).replace(/^[\^~>=<]/, "");
48+
if (locked !== declaredClean && locked !== "missing") continue;
49+
if (locked === "missing") {
50+
drifted.push({ name, declared: declared as string, locked });
51+
}
52+
}
53+
return { drifted };
54+
},
55+
}),
56+
],
57+
output: s.object({
58+
driftedPackages: s.array(s.object({
59+
name: s.string,
60+
declared: s.string,
61+
locked: s.string,
62+
})),
63+
driftCount: s.int,
64+
allInSync: s.boolean,
65+
}),
66+
addons: [repair()],
67+
});
68+
69+
export default lockFileDriftDetector;
70+
```
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# 311 - Git Conflict Marker Scanner
2+
3+
```rig
4+
import { agent, p, s, steering } from "rig";
5+
6+
// Agent role: scan tracked files for unresolved git conflict markers and report affected locations.
7+
const gitConflictMarkerScanner = agent({
8+
model: "small",
9+
instructions: p`You are a git conflict marker scanner.
10+
11+
Check for conflict markers in tracked files:
12+
${p.bash("git ls-files | xargs grep -lrn '<<<<<<< ' 2>/dev/null || echo 'no conflicts found'")}
13+
14+
For files with conflict markers, get the details:
15+
${p.bash("git ls-files | xargs grep -n '^<<<<<<< \\|^=======$\\|^>>>>>>> ' 2>/dev/null || echo 'none'")}
16+
17+
Analyze the results and return the declared output listing all conflict markers found.`,
18+
output: s.object({
19+
conflicts: s.array(s.object({
20+
file: s.path,
21+
line: s.int,
22+
markerType: s.enum("start", "middle", "end"),
23+
})),
24+
affectedFiles: s.array(s.path),
25+
conflictCount: s.int,
26+
isClean: s.boolean,
27+
}),
28+
addons: [steering()],
29+
});
30+
31+
export default gitConflictMarkerScanner;
32+
```
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# 312 - Proto Field Extractor
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
// Agent role: extract field definitions from protobuf message types in .proto files.
8+
const protoFieldExtractor = agent({
9+
model: "small",
10+
instructions: p`You are a protobuf field extractor.
11+
12+
Find .proto files in the workspace:
13+
${p.bash("find . -name '*.proto' -not -path '*/node_modules/*' 2>/dev/null || echo 'no .proto files found'")}
14+
15+
For each .proto file found, call extractProtoFields with its path to parse message definitions.
16+
Return the declared output keyed by message name.`,
17+
tools: [
18+
defineTool("extractProtoFields", {
19+
description: "Read a .proto file and extract message field definitions",
20+
parameters: s.object({ filePath: s.path }),
21+
async handler({ filePath }) {
22+
const content = await readFile(filePath, "utf8");
23+
const result: Record<string, Array<{ fieldName: string; fieldType: string; fieldNumber: number; isRepeated: boolean }>> = {};
24+
const messageRegex = /message\s+(\w+)\s*\{([^}]*)\}/gs;
25+
const fieldRegex = /^\s*(repeated\s+)?(\w+)\s+(\w+)\s*=\s*(\d+)/gm;
26+
for (const msgMatch of content.matchAll(messageRegex)) {
27+
const messageName = msgMatch[1];
28+
const body = msgMatch[2];
29+
const fields: Array<{ fieldName: string; fieldType: string; fieldNumber: number; isRepeated: boolean }> = [];
30+
for (const fldMatch of body.matchAll(fieldRegex)) {
31+
fields.push({
32+
isRepeated: !!fldMatch[1],
33+
fieldType: fldMatch[2],
34+
fieldName: fldMatch[3],
35+
fieldNumber: parseInt(fldMatch[4], 10),
36+
});
37+
}
38+
result[messageName] = fields;
39+
}
40+
return result;
41+
},
42+
}),
43+
],
44+
output: s.record(s.array(s.object({
45+
fieldName: s.string,
46+
fieldType: s.string,
47+
fieldNumber: s.int,
48+
isRepeated: s.boolean,
49+
}))),
50+
addons: [repair()],
51+
});
52+
53+
export default protoFieldExtractor;
54+
```
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# 313 - Git Remote Url Inspector
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
6+
// Agent role: inspect git remote URLs and classify their protocol and type.
7+
const gitRemoteUrlInspector = agent({
8+
model: "small",
9+
instructions: p`You are a git remote URL inspector.
10+
11+
List all remotes and their URLs:
12+
${p.bash("git remote -v 2>/dev/null || echo 'no remotes'")}
13+
14+
Count remote branches on origin:
15+
${p.bash("git ls-remote --heads origin 2>/dev/null | wc -l || echo '0'")}
16+
17+
Use the parseRemoteLine tool for each remote line to extract protocol and type.
18+
Return the declared output.`,
19+
tools: [
20+
defineTool("parseRemoteLine", {
21+
description: "Parse a git remote -v output line and extract name, url, protocol, and type",
22+
parameters: s.object({ line: s.string }),
23+
handler({ line }) {
24+
const match = line.match(/^(\S+)\s+(\S+)\s+\((\w+)\)$/);
25+
if (!match) return null;
26+
const [, name, url, type] = match;
27+
let protocol: "https" | "ssh" | "git" | "file";
28+
if (url.startsWith("https://")) protocol = "https" as const;
29+
else if (url.startsWith("git@") || url.startsWith("ssh://")) protocol = "ssh" as const;
30+
else if (url.startsWith("git://")) protocol = "git" as const;
31+
else protocol = "file" as const;
32+
return { name, url, type, protocol };
33+
},
34+
}),
35+
],
36+
output: s.object({
37+
remotes: s.array(s.object({
38+
name: s.string,
39+
url: s.string,
40+
type: s.string,
41+
protocol: s.enum("https", "ssh", "git", "file"),
42+
})),
43+
remoteBranchCount: s.int,
44+
hasOrigin: s.boolean,
45+
}),
46+
addons: [repair()],
47+
});
48+
49+
export default gitRemoteUrlInspector;
50+
```
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# 314 - Ts Enum Value Extractor
2+
3+
```rig
4+
import { agent, p, s, defineTool, steering } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
// Agent role: extract TypeScript enum declarations and their members from source files.
8+
const tsEnumValueExtractor = agent({
9+
model: "small",
10+
instructions: p`You are a TypeScript enum value extractor.
11+
12+
Find TypeScript files with enum declarations:
13+
${p.bash("grep -rln 'enum ' --include='*.ts' . 2>/dev/null | head -20 || echo 'no enums found'")}
14+
15+
For each file found, call extractEnumValues with the file path to parse all enum declarations.
16+
Return the declared output keyed by enum name.`,
17+
tools: [
18+
defineTool("extractEnumValues", {
19+
description: "Read a TypeScript file and extract all enum declarations with their members",
20+
parameters: s.object({ filePath: s.path }),
21+
async handler({ filePath }) {
22+
const content = await readFile(filePath, "utf8");
23+
const result: Record<string, { members: string[]; isConst: boolean; memberCount: number }> = {};
24+
const enumRegex = /(const\s+)?enum\s+(\w+)\s*\{([^}]*)\}/gs;
25+
for (const match of content.matchAll(enumRegex)) {
26+
const isConst = !!match[1];
27+
const enumName = match[2];
28+
const body = match[3];
29+
const members = body
30+
.split(",")
31+
.map((m: string) => m.trim().split(/\s*=\s*/)[0].trim())
32+
.filter((m: string) => m.length > 0 && !m.startsWith("//"));
33+
result[enumName] = { members, isConst, memberCount: members.length };
34+
}
35+
return result;
36+
},
37+
}),
38+
],
39+
output: s.record(s.object({
40+
members: s.array(s.string),
41+
isConst: s.boolean,
42+
memberCount: s.int,
43+
})),
44+
addons: [steering()],
45+
});
46+
47+
export default tsEnumValueExtractor;
48+
```
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# 315 - Json Config Merger
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
6+
// Agent role: deeply merge two JSON config files and write the result to an output path.
7+
const jsonConfigMerger = agent({
8+
model: "small",
9+
input: s.object({
10+
baseConfig: s.path,
11+
overrideConfig: s.path,
12+
outputPath: s.path,
13+
}),
14+
instructions: p`You are a JSON config merger.
15+
16+
Base config contents:
17+
${p.readInput("baseConfig")}
18+
19+
Override config contents:
20+
${p.readInput("overrideConfig")}
21+
22+
Use the mergeJsonObjects tool to deeply merge the override config into the base config.
23+
Then write the merged result to ${p.writeInput("outputPath", "mergedContent")}.
24+
Return the declared output.`,
25+
tools: [
26+
defineTool("mergeJsonObjects", {
27+
description: "Deep merge two JSON strings, returning the merged JSON and counts of added/overridden keys",
28+
parameters: s.object({
29+
baseJson: s.string,
30+
overrideJson: s.string,
31+
}),
32+
handler({ baseJson, overrideJson }) {
33+
const base = JSON.parse(baseJson) as Record<string, unknown>;
34+
const override = JSON.parse(overrideJson) as Record<string, unknown>;
35+
let keysAdded = 0;
36+
let keysOverridden = 0;
37+
function deepMerge(
38+
target: Record<string, unknown>,
39+
source: Record<string, unknown>
40+
): Record<string, unknown> {
41+
const result = { ...target };
42+
for (const [key, value] of Object.entries(source)) {
43+
if (key in result) {
44+
if (
45+
typeof result[key] === "object" &&
46+
result[key] !== null &&
47+
typeof value === "object" &&
48+
value !== null
49+
) {
50+
result[key] = deepMerge(
51+
result[key] as Record<string, unknown>,
52+
value as Record<string, unknown>
53+
);
54+
} else {
55+
result[key] = value;
56+
keysOverridden++;
57+
}
58+
} else {
59+
result[key] = value;
60+
keysAdded++;
61+
}
62+
}
63+
return result;
64+
}
65+
const merged = deepMerge(base, override);
66+
return {
67+
mergedJson: JSON.stringify(merged, null, 2),
68+
keysAdded,
69+
keysOverridden,
70+
totalKeys: Object.keys(merged).length,
71+
};
72+
},
73+
}),
74+
],
75+
output: s.object({
76+
keysAdded: s.int,
77+
keysOverridden: s.int,
78+
totalKeys: s.int,
79+
outputPath: s.path,
80+
}),
81+
addons: [repair()],
82+
});
83+
84+
export default jsonConfigMerger;
85+
```
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# 316 - Markdown Toc Generator
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
// Agent role: generate a table of contents markdown file from all headings found in workspace .md files.
8+
const markdownTocGenerator = agent({
9+
model: "small",
10+
instructions: p`You are a markdown table of contents generator.
11+
12+
Find all markdown files in the workspace:
13+
${p.glob("**/*.md")}
14+
15+
For each file, call extractHeadings to parse its heading structure.
16+
Then write the combined TOC to ${p.write("TOC.md", "tocContent")}.
17+
Return the declared output.`,
18+
tools: [
19+
defineTool("extractHeadings", {
20+
description: "Read a markdown file and extract all headings with their levels and anchor text",
21+
parameters: s.object({ filePath: s.path }),
22+
async handler({ filePath }) {
23+
const content = await readFile(filePath, "utf8");
24+
const headings: Array<{ level: number; text: string; anchor: string }> = [];
25+
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
26+
for (const match of content.matchAll(headingRegex)) {
27+
const level = match[1].length;
28+
const text = match[2].trim();
29+
const anchor = text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
30+
headings.push({ level, text, anchor });
31+
}
32+
return { filePath, headings };
33+
},
34+
}),
35+
],
36+
output: s.object({
37+
processedFiles: s.array(s.path),
38+
headingCount: s.int,
39+
outputPath: s.path,
40+
tocGenerated: s.boolean,
41+
}),
42+
addons: [repair()],
43+
});
44+
45+
export default markdownTocGenerator;
46+
```

0 commit comments

Comments
 (0)