diff --git a/skills/rig/samples/501-dockerfile-env-inspector.md b/skills/rig/samples/501-dockerfile-env-inspector.md new file mode 100644 index 0000000..ad38ad8 --- /dev/null +++ b/skills/rig/samples/501-dockerfile-env-inspector.md @@ -0,0 +1,43 @@ +# 501 - Dockerfile ENV Inspector + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const extractDockerfileEnv = defineTool("extractDockerfileEnv", { + description: "Extract ENV instructions from a Dockerfile", + parameters: s.object({ filePath: s.string }), + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf8"); + const envVars: Record = {}; + const keyValueRe = /^ENV\s+(\w+)=(\S+)/gm; + const keySpaceRe = /^ENV\s+(\w+)\s+(.+)$/gm; + let m: RegExpExecArray | null; + while ((m = keyValueRe.exec(content)) !== null) { + envVars[m[1]] = m[2]; + } + while ((m = keySpaceRe.exec(content)) !== null) { + if (!(m[1] in envVars)) envVars[m[1]] = m[2].trim(); + } + return envVars; + }, +}); + +// Agent role: Parse ENV instructions from all Dockerfiles in the workspace and classify each variable. +const dockerfileEnvInspector = agent({ + model: "small", + instructions: p`You are given the output of: ${p.bash("find . -name Dockerfile -o -name 'Dockerfile.*' 2>/dev/null | head -20")}. +For each Dockerfile path found, call extractDockerfileEnv to extract its ENV variables. +Classify each variable as build-time (used during build, e.g. VERSION, BUILD_DATE) or runtime (passed to app at runtime). +Return the declared output.`, + output: s.object({ + files: s.record(s.record(s.string)), + totalVars: s.int, + totalFiles: s.int, + }), + tools: [extractDockerfileEnv], + addons: [repair()], +}); + +export default dockerfileEnvInspector; +``` diff --git a/skills/rig/samples/502-npm-script-prefix-analyzer.md b/skills/rig/samples/502-npm-script-prefix-analyzer.md new file mode 100644 index 0000000..4051c34 --- /dev/null +++ b/skills/rig/samples/502-npm-script-prefix-analyzer.md @@ -0,0 +1,39 @@ +# 502 - NPM Script Prefix Analyzer + +```rig +import { agent, p, s, defineTool, steering, repair } from "rig"; + +const classifyScriptPrefix = defineTool("classifyScriptPrefix", { + description: "Classify the command prefix of an npm script", + parameters: s.object({ command: s.string }), + handler: ({ command }: { command: string }) => { + const first = command.trim().split(/\s+/)[0] ?? ""; + if (first === "node") return "node" as const; + if (first === "ts-node" || first === "ts-node-esm") return "ts-node" as const; + if (first === "npx") return "npx" as const; + if (first === "sh" || first === "bash" || first === "zsh" || first.startsWith("./")) return "shell" as const; + return "other" as const; + }, +}); + +// Agent role: Analyze npm script command prefixes in package.json and summarize by prefix class. +const npmScriptPrefixAnalyzer = agent({ + model: "small", + instructions: p`Analyze the npm scripts in: ${p.read("package.json")}. +For each script, call classifyScriptPrefix with the script command. +Return the declared output with per-script details and a summary of prefix counts.`, + output: s.object({ + scripts: s.record(s.object({ + command: s.string, + prefix: s.string, + prefixClass: s.enum("node", "ts-node", "npx", "shell", "other"), + })), + totalScripts: s.int, + prefixCounts: s.record(s.int), + }), + tools: [classifyScriptPrefix], + addons: [steering(), repair()], +}); + +export default npmScriptPrefixAnalyzer; +``` diff --git a/skills/rig/samples/503-ini-config-parser.md b/skills/rig/samples/503-ini-config-parser.md new file mode 100644 index 0000000..beced3b --- /dev/null +++ b/skills/rig/samples/503-ini-config-parser.md @@ -0,0 +1,49 @@ +# 503 - INI Config Parser + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const parseIniSection = defineTool("parseIniSection", { + description: "Parse an INI config file into sections and key-value pairs", + parameters: s.object({ content: s.string }), + handler: ({ content }: { content: string }) => { + const sections: Record> = {}; + let current = "__default__"; + sections[current] = {}; + for (const raw of content.split(/\r?\n/)) { + const line = raw.trim(); + if (!line || line.startsWith(";") || line.startsWith("#")) continue; + const sectionMatch = line.match(/^\[(.+)\]$/); + if (sectionMatch) { + current = sectionMatch[1]; + sections[current] = {}; + continue; + } + const kvMatch = line.match(/^([^=]+)=(.*)$/); + if (kvMatch) { + sections[current][kvMatch[1].trim()] = kvMatch[2].trim(); + } + } + return sections; + }, +}); + +// Agent role: Parse a caller-supplied INI config file and return structured sections and key counts. +const iniConfigParser = agent({ + model: "small", + input: s.object({ configFile: s.string }), + instructions: p`Parse the INI config file at the path provided in input.configFile: ${p.readInput("configFile")}. +Call parseIniSection with the file contents. +Return the declared output.`, + output: s.object({ + sections: s.record(s.record(s.string)), + totalKeys: s.number, + totalSections: s.number, + hasDefaultSection: s.boolean, + }), + tools: [parseIniSection], + addons: [repair()], +}); + +export default iniConfigParser; +``` diff --git a/skills/rig/samples/504-git-reflog-inspector.md b/skills/rig/samples/504-git-reflog-inspector.md new file mode 100644 index 0000000..4b6cece --- /dev/null +++ b/skills/rig/samples/504-git-reflog-inspector.md @@ -0,0 +1,43 @@ +# 504 - Git Reflog Inspector + +```rig +import { agent, p, s, defineTool, steering } from "rig"; + +const classifyReflogEntry = defineTool("classifyReflogEntry", { + description: "Classify a single git reflog entry line", + parameters: s.object({ line: s.string }), + handler: ({ line }: { line: string }) => { + const hashMatch = line.match(/^([0-9a-f]+)/); + const hash = hashMatch ? hashMatch[1] : ""; + const lower = line.toLowerCase(); + let action: "commit" | "merge" | "rebase" | "reset" | "checkout" | "other" = "other"; + if (lower.includes("merge")) action = "merge"; + else if (lower.includes("rebase")) action = "rebase"; + else if (lower.includes("reset")) action = "reset"; + else if (lower.includes("checkout")) action = "checkout"; + else if (lower.includes("commit")) action = "commit"; + return { hash, action }; + }, +}); + +// Agent role: Inspect the last 50 git reflog entries, classify each action, and return counts. +const gitReflogInspector = agent({ + model: "small", + instructions: p`Inspect git reflog: ${p.bash("git reflog --oneline -50 2>/dev/null || echo 'no reflog'")}. +For each line, call classifyReflogEntry to get the hash and action. +Return the declared output.`, + output: s.object({ + entries: s.array(s.object({ + hash: s.string, + action: s.enum("commit", "merge", "rebase", "reset", "checkout", "other"), + message: s.string, + })), + actionCounts: s.record(s.number), + totalEntries: s.number, + }), + tools: [classifyReflogEntry], + addons: [steering()], +}); + +export default gitReflogInspector; +``` diff --git a/skills/rig/samples/505-source-line-length-auditor.md b/skills/rig/samples/505-source-line-length-auditor.md new file mode 100644 index 0000000..de5f44e --- /dev/null +++ b/skills/rig/samples/505-source-line-length-auditor.md @@ -0,0 +1,51 @@ +# 505 - Source Line Length Auditor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const auditLineLengths = defineTool("auditLineLengths", { + description: "Audit line lengths in a source file", + parameters: s.object({ filePath: s.string }), + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf8"); + const lines = content.split(/\r?\n/); + let total = 0; + let maxLength = 0; + let longLineCount = 0; + let veryLongLineCount = 0; + for (const line of lines) { + const len = line.length; + total += len; + if (len > maxLength) maxLength = len; + if (len > 80) longLineCount++; + if (len > 120) veryLongLineCount++; + } + const avgLength = lines.length > 0 ? total / lines.length : 0; + return { avgLength, maxLength, longLineCount, veryLongLineCount }; + }, +}); + +// Agent role: Audit line lengths across all TypeScript source files and identify the most verbose file. +const sourceLineLengthAuditor = agent({ + model: "small", + instructions: p`Audit TypeScript source files found at: ${p.bash("find src -name '*.ts' 2>/dev/null | head -50 || echo 'no src dir'")}. +For each file path, call auditLineLengths to get per-file statistics. +Return the declared output.`, + output: s.object({ + files: s.record(s.object({ + avgLength: s.number, + maxLength: s.number, + longLineCount: s.number, + veryLongLineCount: s.number, + })), + totalFiles: s.number, + globalMaxLength: s.number, + mostVerboseFile: s.optional(s.string), + }), + tools: [auditLineLengths], + addons: [repair()], +}); + +export default sourceLineLengthAuditor; +``` diff --git a/skills/rig/samples/506-npm-script-dep-workflow.md b/skills/rig/samples/506-npm-script-dep-workflow.md new file mode 100644 index 0000000..edc0af1 --- /dev/null +++ b/skills/rig/samples/506-npm-script-dep-workflow.md @@ -0,0 +1,60 @@ +# 506 - NPM Script Dep Workflow + +```rig +import { agent, workflow, p, s } from "rig"; + +// Agent role: Read package.json and build a graph of npm script dependencies via pre/post hooks. +const scriptGraphBuilder = agent({ + name: "scriptGraphBuilder", + model: "small", + instructions: p`Read package.json: ${p.read("package.json")}. +Extract all npm scripts. For each script, identify its pre/post hook dependencies (e.g., "prebuild" runs before "build"). +Return the declared output.`, + output: s.object({ + scripts: s.record(s.object({ + command: s.string, + deps: s.array(s.string), + })), + totalScripts: s.int, + }), +}); + +// Agent role: Detect cycles in a script dependency graph using DFS. +const cycleDetector = agent({ + name: "cycleDetector", + model: "small", + input: s.object({ + scripts: s.record(s.object({ + command: s.string, + deps: s.array(s.string), + })), + totalScripts: s.int, + }), + instructions: `Analyze the provided script dependency graph for cycles using DFS. +Return whether cycles exist, list any cycles found, and the total node count.`, + output: s.object({ + hasCycles: s.boolean, + cycles: s.array(s.array(s.string)), + totalNodes: s.int, + }), +}); + +// Workflow role: Build npm script dependency graph then detect cycles. +const npmScriptDepWorkflow = workflow({ + meta: { name: "npm-script-dep-workflow", description: "Analyze npm script pre/post dependency graph and detect cycles" }, + body: async ({ call }) => { + const graph = await call(scriptGraphBuilder, "build script graph from package.json"); + const scripts = graph?.scripts ?? {}; + const totalScripts = graph?.totalScripts ?? 0; + const cycles = await call(cycleDetector, { scripts, totalScripts }); + const hasCycles = cycles?.hasCycles ?? false; + const cycleList = cycles?.cycles ?? []; + let recommendation: "safe" | "has-cycles" | "empty" = "safe"; + if (totalScripts === 0) recommendation = "empty"; + else if (hasCycles) recommendation = "has-cycles"; + return { scriptCount: totalScripts, hasCycles, cycles: cycleList, recommendation }; + }, +}); + +export default npmScriptDepWorkflow; +``` diff --git a/skills/rig/samples/507-html-form-field-extractor.md b/skills/rig/samples/507-html-form-field-extractor.md new file mode 100644 index 0000000..e7bfabf --- /dev/null +++ b/skills/rig/samples/507-html-form-field-extractor.md @@ -0,0 +1,92 @@ +# 507 - HTML Form Field Extractor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const extractFormFields = defineTool("extractFormFields", { + description: "Extract HTML form fields from HTML content", + parameters: s.object({ content: s.string }), + handler: ({ content }: { content: string }) => { + type FieldType = "text" | "email" | "password" | "checkbox" | "radio" | "select" | "textarea" | "other"; + const fields: Array<{ name?: string; type: FieldType; required: boolean; id?: string }> = []; + const inputRe = /]*)>/gi; + const selectRe = /]*)>/gi; + const textareaRe = /]*)>/gi; + const getAttr = (attrs: string, attr: string): string | undefined => { + const m = attrs.match(new RegExp(`${attr}\\s*=\\s*["']?([^"'\\s>]*)`, "i")); + return m ? m[1] : undefined; + }; + const classifyType = (t?: string): FieldType => { + if (!t) return "text"; + const lower = t.toLowerCase(); + if (lower === "email") return "email"; + if (lower === "password") return "password"; + if (lower === "checkbox") return "checkbox"; + if (lower === "radio") return "radio"; + if (lower === "text") return "text"; + return "other"; + }; + let m: RegExpExecArray | null; + while ((m = inputRe.exec(content)) !== null) { + const attrs = m[1]; + const entry: { name?: string; type: FieldType; required: boolean; id?: string } = { + type: classifyType(getAttr(attrs, "type")), + required: /\brequired\b/i.test(attrs), + }; + const name = getAttr(attrs, "name"); + if (name !== undefined) entry.name = name; + const id = getAttr(attrs, "id"); + if (id !== undefined) entry.id = id; + fields.push(entry); + } + while ((m = selectRe.exec(content)) !== null) { + const attrs = m[1]; + const entry: { name?: string; type: FieldType; required: boolean; id?: string } = { + type: "select" as const, + required: /\brequired\b/i.test(attrs), + }; + const name = getAttr(attrs, "name"); + if (name !== undefined) entry.name = name; + const id = getAttr(attrs, "id"); + if (id !== undefined) entry.id = id; + fields.push(entry); + } + while ((m = textareaRe.exec(content)) !== null) { + const attrs = m[1]; + const entry: { name?: string; type: FieldType; required: boolean; id?: string } = { + type: "textarea" as const, + required: /\brequired\b/i.test(attrs), + }; + const name = getAttr(attrs, "name"); + if (name !== undefined) entry.name = name; + const id = getAttr(attrs, "id"); + if (id !== undefined) entry.id = id; + fields.push(entry); + } + return fields; + }, +}); + +// Agent role: Extract and classify HTML form fields from a caller-supplied HTML file. +const htmlFormFieldExtractor = agent({ + model: "small", + input: s.object({ htmlFile: s.string }), + instructions: p`Extract form fields from the HTML file at input.htmlFile: ${p.readInput("htmlFile")}. +Call extractFormFields with the file content. +Return the declared output.`, + output: s.object({ + fields: s.array(s.object({ + name: s.optional(s.string), + type: s.enum("text", "email", "password", "checkbox", "radio", "select", "textarea", "other"), + required: s.boolean, + id: s.optional(s.string), + })), + totalFields: s.int, + fieldTypeCounts: s.record(s.int), + }), + tools: [extractFormFields], + addons: [repair()], +}); + +export default htmlFormFieldExtractor; +``` diff --git a/skills/rig/samples/508-dotenv-template-validator.md b/skills/rig/samples/508-dotenv-template-validator.md new file mode 100644 index 0000000..df4acc9 --- /dev/null +++ b/skills/rig/samples/508-dotenv-template-validator.md @@ -0,0 +1,47 @@ +# 508 - Dotenv Template Validator + +```rig +import { agent, p, s, defineTool, steering } from "rig"; + +const compareEnvKeys = defineTool("compareEnvKeys", { + description: "Compare env keys between a template and actual .env file", + parameters: s.object({ template: s.string, actual: s.string }), + handler: ({ template, actual }: { template: string; actual: string }) => { + const parseKeys = (content: string) => + content.split(/\r?\n/) + .map((l: string) => l.trim()) + .filter((l: string) => l && !l.startsWith("#")) + .map((l: string) => l.split("=")[0].trim()) + .filter(Boolean); + const templateKeys = new Set(parseKeys(template)); + const actualKeys = new Set(parseKeys(actual)); + const missingInEnv = [...templateKeys].filter((k: string) => !actualKeys.has(k)); + const extraInEnv = [...actualKeys].filter((k: string) => !templateKeys.has(k)); + const completenessScore = templateKeys.size > 0 + ? Math.round(((templateKeys.size - missingInEnv.length) / templateKeys.size) * 100) + : 100; + return { missingInEnv, extraInEnv, totalTemplate: templateKeys.size, totalActual: actualKeys.size, completenessScore }; + }, +}); + +// Agent role: Compare .env.example template keys against actual .env and report completeness. +const dotenvTemplateValidator = agent({ + model: "small", + instructions: p`Compare env template ${p.read(".env.example")} with actual env ${p.readOptional(".env", "")}. +Call compareEnvKeys with both file contents. +Determine the status: complete (no missing, no extra), missing-keys, extra-keys, or mismatch (both). +Return the declared output.`, + output: s.object({ + missingInEnv: s.array(s.string), + extraInEnv: s.array(s.string), + totalTemplate: s.int, + totalActual: s.int, + completenessScore: s.number, + status: s.enum("complete", "missing-keys", "extra-keys", "mismatch"), + }), + tools: [compareEnvKeys], + addons: [steering()], +}); + +export default dotenvTemplateValidator; +``` diff --git a/skills/rig/samples/509-ts-namespace-usage-reporter.md b/skills/rig/samples/509-ts-namespace-usage-reporter.md new file mode 100644 index 0000000..b8c4716 --- /dev/null +++ b/skills/rig/samples/509-ts-namespace-usage-reporter.md @@ -0,0 +1,56 @@ +# 509 - TS Namespace Usage Reporter + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const scanNamespaces = defineTool("scanNamespaces", { + description: "Scan a TypeScript file for namespace declarations and usages", + parameters: s.object({ filePath: s.string }), + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf8"); + const results: Record = {}; + const declRe = /\bnamespace\s+(\w+)/g; + const augRe = /declare\s+(?:module|namespace)\s+['"]?([\w.]+)/g; + const accessRe = /\b([A-Z]\w*)(?:\.\w+){1,}/g; + let m: RegExpExecArray | null; + while ((m = declRe.exec(content)) !== null) { + const ns = m[1]; + if (!results[ns]) results[ns] = { declarationCount: 0, accessCount: 0, augmentationCount: 0 }; + results[ns].declarationCount++; + } + while ((m = augRe.exec(content)) !== null) { + const ns = m[1].split(".")[0]; + if (!results[ns]) results[ns] = { declarationCount: 0, accessCount: 0, augmentationCount: 0 }; + results[ns].augmentationCount++; + } + while ((m = accessRe.exec(content)) !== null) { + const ns = m[1]; + if (results[ns]) results[ns].accessCount++; + } + return results; + }, +}); + +// Agent role: Report TypeScript namespace declarations and access patterns across all source files. +const tsNamespaceUsageReporter = agent({ + model: "small", + instructions: p`Scan TypeScript files: ${p.glob("src/**/*.ts")}. +For each file path, call scanNamespaces to detect namespace usages. +Aggregate results across all files and identify the most used namespace. +Return the declared output.`, + output: s.object({ + namespaces: s.record(s.object({ + declarationCount: s.int, + accessCount: s.int, + augmentationCount: s.int, + })), + totalFiles: s.int, + mostUsedNamespace: s.optional(s.string), + }), + tools: [scanNamespaces], + addons: [repair()], +}); + +export default tsNamespaceUsageReporter; +``` diff --git a/skills/rig/samples/510-multi-stage-artifact-pipeline.md b/skills/rig/samples/510-multi-stage-artifact-pipeline.md new file mode 100644 index 0000000..232ae06 --- /dev/null +++ b/skills/rig/samples/510-multi-stage-artifact-pipeline.md @@ -0,0 +1,62 @@ +# 510 - Multi Stage Artifact Pipeline + +```rig +import { agent, workflow, p, s } from "rig"; + +// Agent role: Discover build artifacts in the dist directory. +const artifactDiscoverer = agent({ + name: "artifactDiscoverer", + model: "small", + instructions: p`List build artifacts: ${p.bash("find dist -type f \\( -name '*.js' -o -name '*.d.ts' -o -name '*.js.map' \\) 2>/dev/null | head -100 || echo 'no dist'")}. +For each file, report its path, approximate size (use 0 if unknown), and file extension type. +Return the declared output.`, + output: s.object({ + artifacts: s.array(s.object({ + path: s.path, + sizeBytes: s.int, + type: s.string, + })), + totalFound: s.int, + }), +}); + +// Agent role: Classify build artifacts by kind (esm, cjs, declaration, sourcemap, other). +const artifactClassifier = agent({ + name: "artifactClassifier", + model: "small", + input: s.object({ + artifacts: s.array(s.object({ + path: s.path, + sizeBytes: s.int, + type: s.string, + })), + totalFound: s.int, + }), + instructions: `Classify each artifact: .d.ts → declaration, .js.map → sourcemap, .mjs or esm/**/*.js → esm, .cjs or cjs/**/*.js → cjs, else other. +Return the classified list and a byType count.`, + output: s.object({ + classified: s.array(s.object({ + path: s.path, + kind: s.enum("esm", "cjs", "declaration", "sourcemap", "other"), + })), + byType: s.record(s.int), + }), +}); + +// Workflow role: Discover, classify, and report on dist build artifacts in three sequential stages. +const multiStageArtifactPipeline = workflow({ + meta: { name: "multi-stage-artifact-pipeline", description: "Three-stage pipeline to discover, classify, and report dist build artifacts" }, + body: async ({ call }) => { + const discovered = await call(artifactDiscoverer, "discover dist artifacts"); + const artifacts = discovered?.artifacts ?? []; + const totalFound = discovered?.totalFound ?? 0; + const classified = await call(artifactClassifier, { artifacts, totalFound }); + const byType = classified?.byType ?? {}; + const totalArtifacts = classified?.classified?.length ?? 0; + const summary = `Found ${totalArtifacts} artifacts: ${Object.entries(byType).map(([k, v]) => `${v} ${k}`).join(", ")}`; + return { reportPath: "dist/artifact-report.json", totalArtifacts, byType, summary }; + }, +}); + +export default multiStageArtifactPipeline; +```