From 0b30a4bc85e53ea232ab8f661025051fcc2a37b5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 08:21:01 +0000 Subject: [PATCH] Add 10 rig samples 361-370 (2026-08-04) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-ts-interface-inheritance-graph-builder.md | 57 ++++++++++++++ .../362-dep-license-compatibility-matrix.md | 60 +++++++++++++++ .../samples/363-tsconfig-options-analyzer.md | 56 ++++++++++++++ .../364-git-patch-format-summarizer.md | 51 +++++++++++++ .../365-npm-lifecycle-script-analyzer.md | 59 +++++++++++++++ .../samples/366-ts-jsdoc-coverage-checker.md | 69 +++++++++++++++++ .../samples/367-html-anchor-link-extractor.md | 62 ++++++++++++++++ .../368-ts-optional-chaining-counter.md | 53 +++++++++++++ .../369-git-file-size-change-tracker.md | 57 ++++++++++++++ .../samples/370-json-pretty-printer-stats.md | 74 +++++++++++++++++++ 10 files changed, 598 insertions(+) create mode 100644 skills/rig/samples/361-ts-interface-inheritance-graph-builder.md create mode 100644 skills/rig/samples/362-dep-license-compatibility-matrix.md create mode 100644 skills/rig/samples/363-tsconfig-options-analyzer.md create mode 100644 skills/rig/samples/364-git-patch-format-summarizer.md create mode 100644 skills/rig/samples/365-npm-lifecycle-script-analyzer.md create mode 100644 skills/rig/samples/366-ts-jsdoc-coverage-checker.md create mode 100644 skills/rig/samples/367-html-anchor-link-extractor.md create mode 100644 skills/rig/samples/368-ts-optional-chaining-counter.md create mode 100644 skills/rig/samples/369-git-file-size-change-tracker.md create mode 100644 skills/rig/samples/370-json-pretty-printer-stats.md diff --git a/skills/rig/samples/361-ts-interface-inheritance-graph-builder.md b/skills/rig/samples/361-ts-interface-inheritance-graph-builder.md new file mode 100644 index 0000000..d664972 --- /dev/null +++ b/skills/rig/samples/361-ts-interface-inheritance-graph-builder.md @@ -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; +``` diff --git a/skills/rig/samples/362-dep-license-compatibility-matrix.md b/skills/rig/samples/362-dep-license-compatibility-matrix.md new file mode 100644 index 0000000..b5798dc --- /dev/null +++ b/skills/rig/samples/362-dep-license-compatibility-matrix.md @@ -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(); + 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; +``` diff --git a/skills/rig/samples/363-tsconfig-options-analyzer.md b/skills/rig/samples/363-tsconfig-options-analyzer.md new file mode 100644 index 0000000..313aa7d --- /dev/null +++ b/skills/rig/samples/363-tsconfig-options-analyzer.md @@ -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; +``` diff --git a/skills/rig/samples/364-git-patch-format-summarizer.md b/skills/rig/samples/364-git-patch-format-summarizer.md new file mode 100644 index 0000000..ec5246b --- /dev/null +++ b/skills/rig/samples/364-git-patch-format-summarizer.md @@ -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 "). +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; +``` diff --git a/skills/rig/samples/365-npm-lifecycle-script-analyzer.md b/skills/rig/samples/365-npm-lifecycle-script-analyzer.md new file mode 100644 index 0000000..ec2210b --- /dev/null +++ b/skills/rig/samples/365-npm-lifecycle-script-analyzer.md @@ -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; +``` diff --git a/skills/rig/samples/366-ts-jsdoc-coverage-checker.md b/skills/rig/samples/366-ts-jsdoc-coverage-checker.md new file mode 100644 index 0000000..5730bab --- /dev/null +++ b/skills/rig/samples/366-ts-jsdoc-coverage-checker.md @@ -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("*")) { + 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; +``` diff --git a/skills/rig/samples/367-html-anchor-link-extractor.md b/skills/rig/samples/367-html-anchor-link-extractor.md new file mode 100644 index 0000000..8fd7bfb --- /dev/null +++ b/skills/rig/samples/367-html-anchor-link-extractor.md @@ -0,0 +1,62 @@ +# 367 - HTML Anchor Link 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 as internal, external, or fragment.", + parameters: { filePath: s.path }, + handler: async ({ filePath }: { filePath: string }) => { + try { + const content = await readFile(filePath, "utf-8"); + const re = /href=["']([^"']+)["']/gi; + const results: Array<{ href: string; type: "internal" | "external" | "fragment" }> = []; + let m: RegExpExecArray | null; + while ((m = re.exec(content)) !== null) { + const href = m[1]; + let type: "internal" | "external" | "fragment" = "internal"; + if (href.startsWith("#")) type = "fragment"; + else if (/^https?:\/\//.test(href) || href.startsWith("//")) type = "external"; + results.push({ href, type }); + } + return results; + } catch { + return []; + } + }, +}); + +const htmlAnchorLinkExtractor = agent({ + model: "small", + instructions: p`Extract and classify all anchor links from HTML files. + +HTML files found: +${p.bash("find . -name '*.html' -not -path '*/node_modules/*' | head -30")} + +Steps: +1. For each HTML file path, call extractAnchors to get the list of hrefs with their types. +2. Flatten all results into a links array, adding sourceFile to each entry. +3. totalLinks = total count. +4. externalCount = links with type "external". +5. internalCount = links with type "internal". +6. fragmentCount = links with type "fragment".`, + output: s.object({ + links: s.array( + s.object({ + href: s.string, + type: s.enum("internal", "external", "fragment"), + sourceFile: s.string, + }) + ), + totalLinks: s.number, + externalCount: s.number, + internalCount: s.number, + fragmentCount: s.number, + }), + tools: [extractAnchors], + addons: [repair()], +}); + +export default htmlAnchorLinkExtractor; +``` diff --git a/skills/rig/samples/368-ts-optional-chaining-counter.md b/skills/rig/samples/368-ts-optional-chaining-counter.md new file mode 100644 index 0000000..6024b03 --- /dev/null +++ b/skills/rig/samples/368-ts-optional-chaining-counter.md @@ -0,0 +1,53 @@ +# 368 - TS Optional Chaining Counter + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFile } from "node:fs/promises"; +import { steering } from "rig"; + +const countNullSafetyOperators = defineTool("countNullSafetyOperators", { + description: "Count optional chaining (?.) and nullish coalescing (??) operators in a TypeScript file.", + parameters: { filePath: s.path }, + handler: async ({ 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 }; + } + }, +}); + +const tsOptionalChainingCounter = agent({ + model: "small", + instructions: p`Count optional chaining (?.) and nullish coalescing (??) operator usage across TypeScript files. + +TypeScript files: +${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -25")} + +Steps: +1. For each file path, call countNullSafetyOperators to get per-file counts. +2. Build files record keyed by file path with optionalChainingCount, nullishCoalescingCount, total. +3. totalOptionalChaining = sum of all optionalChainingCount. +4. totalNullishCoalescing = sum of all nullishCoalescingCount. +5. mostUsedFile = file path with the highest total (omit if all totals are 0).`, + output: s.object({ + files: s.record( + s.object({ + optionalChainingCount: s.number, + nullishCoalescingCount: s.number, + total: s.number, + }) + ), + totalOptionalChaining: s.number, + totalNullishCoalescing: s.number, + mostUsedFile: s.optional(s.string), + }), + tools: [countNullSafetyOperators], + addons: [steering()], +}); + +export default tsOptionalChainingCounter; +``` diff --git a/skills/rig/samples/369-git-file-size-change-tracker.md b/skills/rig/samples/369-git-file-size-change-tracker.md new file mode 100644 index 0000000..c0f9f1d --- /dev/null +++ b/skills/rig/samples/369-git-file-size-change-tracker.md @@ -0,0 +1,57 @@ +# 369 - Git File Size Change Tracker + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const getFileSizeAtRevision = defineTool("getFileSizeAtRevision", { + description: "Get the byte size of a file at a specific git revision.", + parameters: { revision: s.string, filePath: s.string }, + handler: ({ revision, filePath }: { revision: string; filePath: string }) => { + const { execSync } = require("node:child_process"); + try { + const size = parseInt( + execSync(`git cat-file -s ${revision}:${filePath} 2>/dev/null || echo '0'`, { encoding: "utf-8" }).trim(), + 10 + ); + return { size: isNaN(size) ? 0 : size }; + } catch { + return { size: 0 }; + } + }, +}); + +const gitFileSizeChangeTracker = agent({ + model: "small", + instructions: p`Track file size changes between the previous commit and HEAD. + +Files changed in the last commit: +${p.bash("git diff --name-only HEAD~1 HEAD 2>/dev/null || echo ''")} + +Steps: +1. For each changed file path, call getFileSizeAtRevision twice: once with revision "HEAD~1" and once with "HEAD". +2. Compute delta = currentSize - previousSize. +3. Classify change: "grew" if delta > 0, "shrank" if delta < 0, "unchanged" if delta == 0. +4. Build files array with path, previousSize, currentSize, delta, change. +5. totalFiles = files.length. +6. largestGrowth = path with the highest positive delta (omit if none grew). +7. largestShrink = path with the most negative delta (omit if none shrank).`, + output: s.object({ + files: s.array( + s.object({ + path: s.string, + previousSize: s.number, + currentSize: s.number, + delta: s.number, + change: s.enum("grew", "shrank", "unchanged"), + }) + ), + totalFiles: s.number, + largestGrowth: s.optional(s.string), + largestShrink: s.optional(s.string), + }), + tools: [getFileSizeAtRevision], + addons: [repair()], +}); + +export default gitFileSizeChangeTracker; +``` diff --git a/skills/rig/samples/370-json-pretty-printer-stats.md b/skills/rig/samples/370-json-pretty-printer-stats.md new file mode 100644 index 0000000..f6b542a --- /dev/null +++ b/skills/rig/samples/370-json-pretty-printer-stats.md @@ -0,0 +1,74 @@ +# 370 - JSON Pretty Printer Stats + +```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, returning its contents.", + parameters: { filePath: s.path }, + handler: async ({ filePath }: { filePath: string }) => { + const content = await readFile(filePath, "utf-8"); + return JSON.parse(content); + }, +}); + +const analyzeJsonStructure = defineTool("analyzeJsonStructure", { + description: "Analyze the structure of a JSON value and return key count, depth, array count, and object count.", + parameters: { 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.entries(val as Record)) { + 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: { filePath: s.path, content: s.unknown }, + handler: async ({ filePath, content }: { filePath: string; content: unknown }) => { + await writeFile(filePath, JSON.stringify(content, null, 2), "utf-8"); + return { written: true }; + }, +}); + +const jsonPrettyPrinterStats = agent({ + model: "small", + input: s.object({ inputFile: s.string, outputFile: s.string }), + instructions: `Pretty-print a JSON file and report structural statistics. + +Steps: +1. Call readJsonFile with the inputFile from input to get the parsed JSON. +2. Call analyzeJsonStructure with the parsed JSON to get keyCount, depth, arrayCount, objectCount. +3. Call writeJsonFile with outputFile and the parsed JSON to write the pretty-printed output. +4. Return keyCount, depth, arrayCount, objectCount, outputFile (from input), prettyPrinted: true.`, + output: s.object({ + keyCount: s.number, + depth: s.number, + arrayCount: s.number, + objectCount: s.number, + outputFile: s.string, + prettyPrinted: s.boolean, + }), + tools: [readJsonFile, analyzeJsonStructure, writeJsonFile], + addons: [repair()], +}); + +export default jsonPrettyPrinterStats; +```