{"version":3,"file":"parsing.mjs","names":[],"sources":["../../../src/batteries/tools/parsing/index.ts"],"sourcesContent":["/**\n * Pre-constructed tools for parsing CSV, TSV, JSON, YAML, and other structured text formats.\n *\n * @module @nhtio/adk/batteries/tools/parsing\n *\n * @remarks\n * Pre-constructed bundled tools for the `parsing` category. Import individually, the whole\n * category, or import every tool via `@nhtio/adk/batteries`.\n */\n\nimport { load as parseYaml } from 'js-yaml'\nimport { default as Papa } from 'papaparse'\nimport { isError } from '@nhtio/adk/guards'\nimport { validator } from '@nhtio/validation'\nimport { Tool, SpooledJsonArtifact } from '@nhtio/adk/common'\n\n/**\n * Parse a CSV or TSV string into a JSON array.\n *\n * @remarks\n * With `has_header: true` (default), each row becomes an object keyed by column name. Without a\n * header, rows are returned as positional arrays. `delimiter` auto-detects when omitted; pass\n * `\"\\t\"` for TSV. Rows are clipped to `limit` (default 1000, max 10000). Parse warnings are\n * prepended to the output.\n */\nexport const parseCsvTool = new Tool({\n  name: 'parse_csv',\n  description:\n    'Parse a CSV or TSV string into a JSON array. With a header row, returns objects keyed by column name. Without, returns arrays of values.',\n  inputSchema: validator.object({\n    text: validator.string().required().description('CSV or TSV text to parse'),\n    has_header: validator\n      .boolean()\n      .default(true)\n      .description('First row is a header row (default: true)'),\n    delimiter: validator\n      .string()\n      .default('')\n      .allow('')\n      .description(\n        'Field delimiter — auto-detected when omitted or sent as an empty string. Use \"\\\\t\" for TSV.'\n      ),\n    limit: validator.number().default(1000).description('Maximum rows to return (default: 1000)'),\n  }),\n  artifactConstructor: () => SpooledJsonArtifact,\n  handler: async (args) => {\n    const {\n      text,\n      has_header: hasHeader,\n      delimiter,\n      limit: rawLimit,\n    } = args as {\n      text: string\n      has_header: boolean\n      delimiter: string\n      limit: number\n    }\n    const limit = Math.min(10_000, Math.max(1, Math.floor(rawLimit)))\n\n    try {\n      const result = Papa.parse<unknown>(text.trim(), {\n        header: hasHeader,\n        delimiter: delimiter || undefined,\n        skipEmptyLines: true,\n        dynamicTyping: true,\n      })\n\n      const rows = (result.data as unknown[]).slice(0, limit)\n      const warnings =\n        result.errors.length > 0\n          ? `Parse warnings: ${result.errors\n              .slice(0, 3)\n              .map((e) => e.message)\n              .join('; ')}\\n\\n`\n          : ''\n      const truncated =\n        result.data.length > limit ? `\\n\\n(Showing ${limit} of ${result.data.length} rows)` : ''\n\n      return warnings + JSON.stringify(rows, null, 2) + truncated\n    } catch (err) {\n      return `Error: ${isError(err) ? err.message : String(err)}`\n    }\n  },\n})\n\n/**\n * Parse a YAML string into JSON.\n *\n * @remarks\n * Returns a pretty-printed JSON representation of the parsed YAML document. Invalid YAML\n * returns an error string.\n */\nexport const parseYamlTool = new Tool({\n  name: 'parse_yaml',\n  description: 'Parse a YAML string and return the equivalent JSON.',\n  inputSchema: validator.object({\n    text: validator.string().required().description('YAML text to parse'),\n  }),\n  artifactConstructor: () => SpooledJsonArtifact,\n  handler: async (args) => {\n    const { text } = args as { text: string }\n    try {\n      const parsed = parseYaml(text)\n      // An empty / whitespace-only / BOM-only document parses to `undefined`, and\n      // `JSON.stringify(undefined)` returns the JS value `undefined` (not a string), breaking the\n      // tool's string-return contract. Normalise that empty document to JSON `null`.\n      if (parsed === undefined) return 'null'\n      // YAML permits .NaN / .inf / -.inf, which `JSON.stringify` would silently turn into `null`,\n      // losing the value. Render non-finite numbers as their YAML token strings so the information\n      // survives the JSON round-trip instead of being corrupted to null.\n      return JSON.stringify(\n        parsed,\n        (_key, value) => {\n          if (typeof value === 'number' && !Number.isFinite(value)) {\n            return Number.isNaN(value) ? '.NaN' : value > 0 ? '.inf' : '-.inf'\n          }\n          return value\n        },\n        2\n      )\n    } catch (err) {\n      return `Error: Invalid YAML — ${isError(err) ? err.message : String(err)}`\n    }\n  },\n})\n\n/**\n * Extract key-value pairs from text.\n *\n * @remarks\n * Handles `.env` files, config files, and query strings. `kv_delimiter` chooses the\n * key→value separator (`=`, `:`, or `auto`); `pair_delimiter` selects how pairs are separated\n * (`newline`, `comma`, `semicolon`, `ampersand`). Surrounding single or double quotes around\n * values are stripped. Comment lines starting with `#` are skipped by default.\n */\nexport const parseKvTool = new Tool({\n  name: 'parse_kv',\n  description:\n    'Extract key-value pairs from text (e.g. .env files, config files, query strings). Returns a JSON object.',\n  inputSchema: validator.object({\n    text: validator.string().required().description('Text containing key-value pairs'),\n    kv_delimiter: validator\n      .string()\n      .valid('=', ':', 'auto')\n      .default('auto')\n      .description('Separator between key and value (default: auto-detect)'),\n    pair_delimiter: validator\n      .string()\n      .valid('newline', 'comma', 'semicolon', 'ampersand')\n      .default('newline')\n      .description('Separator between pairs (default: newline)'),\n    skip_comments: validator\n      .boolean()\n      .default(true)\n      .description('Skip lines starting with # (default: true)'),\n  }),\n  artifactConstructor: () => SpooledJsonArtifact,\n  handler: async (args) => {\n    const {\n      text,\n      kv_delimiter: kvDelim,\n      pair_delimiter: pairDelimKey,\n      skip_comments: skipComments,\n    } = args as {\n      text: string\n      kv_delimiter: string\n      pair_delimiter: string\n      skip_comments: boolean\n    }\n\n    const pairDelimMap: Record<string, string> = {\n      newline: '\\n',\n      comma: ',',\n      semicolon: ';',\n      ampersand: '&',\n    }\n    const pairDelim = pairDelimMap[pairDelimKey] ?? '\\n'\n\n    const result: Record<string, string> = {}\n\n    for (const raw of text.split(pairDelim)) {\n      const line = raw.trim()\n      if (line === '') continue\n      if (skipComments && line.startsWith('#')) continue\n\n      const delim =\n        kvDelim === 'auto' ? (line.includes('=') ? '=' : line.includes(':') ? ':' : null) : kvDelim\n      if (!delim) continue\n\n      const sepIdx = line.indexOf(delim)\n      if (sepIdx === -1) continue\n\n      const key = line.slice(0, sepIdx).trim()\n      let value = line.slice(sepIdx + delim.length).trim()\n\n      if (\n        (value.startsWith('\"') && value.endsWith('\"')) ||\n        (value.startsWith(\"'\") && value.endsWith(\"'\"))\n      ) {\n        value = value.slice(1, -1)\n      }\n\n      if (key) result[key] = value\n    }\n\n    return JSON.stringify(result, null, 2)\n  },\n})\n\n/**\n * Detect the most likely field delimiter in a CSV-like text sample.\n *\n * @remarks\n * Tries comma, tab, semicolon, pipe, and colon. Scores each by mean field count divided by\n * variance (so consistent row widths beat noisy splits). Looks at up to the first 20 lines of\n * the first 5000 characters.\n */\nexport const detectDelimiterTool = new Tool({\n  name: 'detect_delimiter',\n  description: 'Detect the most likely field delimiter in a CSV-like text sample.',\n  inputSchema: validator.object({\n    text: validator.string().required().description('Sample of the delimited text to analyze'),\n  }),\n  handler: async (args) => {\n    const { text } = args as { text: string }\n    const sample = text.slice(0, 5000)\n    const lines = sample\n      .split('\\n')\n      .map((l) => l.trim())\n      .filter((l) => l.length > 0)\n      .slice(0, 20)\n\n    if (lines.length === 0) return 'Error: No lines to analyze.'\n\n    const candidates: Array<{ delim: string; name: string }> = [\n      { delim: ',', name: 'comma' },\n      { delim: '\\t', name: 'tab' },\n      { delim: ';', name: 'semicolon' },\n      { delim: '|', name: 'pipe' },\n      { delim: ':', name: 'colon' },\n    ]\n\n    let best = { name: 'comma', delim: ',', score: -1, avgFields: 0 }\n\n    for (const { delim, name } of candidates) {\n      const counts = lines.map((l) => l.split(delim).length - 1)\n      const avg = counts.reduce((a, b) => a + b, 0) / counts.length\n      if (avg === 0) continue\n      const fieldVariance = counts.reduce((a, b) => a + (b - avg) ** 2, 0) / counts.length\n      const score = avg / (1 + fieldVariance)\n      if (score > best.score) {\n        best = { name, delim, score, avgFields: Number.parseFloat((avg + 1).toFixed(1)) }\n      }\n    }\n\n    return `Detected delimiter: ${best.name} (\"${best.delim === '\\t' ? '\\\\t' : best.delim}\") — ~${best.avgFields} fields per row`\n  },\n})\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,eAAe,IAAI,KAAK;CACnC,MAAM;CACN,aACE;CACF,aAAa,UAAU,OAAO;EAC5B,MAAM,UAAU,OAAO,EAAE,SAAS,EAAE,YAAY,0BAA0B;EAC1E,YAAY,UACT,QAAQ,EACR,QAAQ,IAAI,EACZ,YAAY,2CAA2C;EAC1D,WAAW,UACR,OAAO,EACP,QAAQ,EAAE,EACV,MAAM,EAAE,EACR,YACC,+FACF;EACF,OAAO,UAAU,OAAO,EAAE,QAAQ,GAAI,EAAE,YAAY,wCAAwC;CAC9F,CAAC;CACD,2BAA2B;CAC3B,SAAS,OAAO,SAAS;EACvB,MAAM,EACJ,MACA,YAAY,WACZ,WACA,OAAO,aACL;EAMJ,MAAM,QAAQ,KAAK,IAAI,KAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC;EAEhE,IAAI;GACF,MAAM,SAAS,KAAK,MAAe,KAAK,KAAK,GAAG;IAC9C,QAAQ;IACR,WAAW,aAAa,KAAA;IACxB,gBAAgB;IAChB,eAAe;GACjB,CAAC;GAED,MAAM,OAAQ,OAAO,KAAmB,MAAM,GAAG,KAAK;GACtD,MAAM,WACJ,OAAO,OAAO,SAAS,IACnB,mBAAmB,OAAO,OACvB,MAAM,GAAG,CAAC,EACV,KAAK,MAAM,EAAE,OAAO,EACpB,KAAK,IAAI,EAAE,QACd;GACN,MAAM,YACJ,OAAO,KAAK,SAAS,QAAQ,gBAAgB,MAAM,MAAM,OAAO,KAAK,OAAO,UAAU;GAExF,OAAO,WAAW,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI;EACpD,SAAS,KAAK;GACZ,OAAO,UAAU,QAAQ,GAAG,IAAI,IAAI,UAAU,OAAO,GAAG;EAC1D;CACF;AACF,CAAC;;;;;;;;AASD,IAAa,gBAAgB,IAAI,KAAK;CACpC,MAAM;CACN,aAAa;CACb,aAAa,UAAU,OAAO,EAC5B,MAAM,UAAU,OAAO,EAAE,SAAS,EAAE,YAAY,oBAAoB,EACtE,CAAC;CACD,2BAA2B;CAC3B,SAAS,OAAO,SAAS;EACvB,MAAM,EAAE,SAAS;EACjB,IAAI;GACF,MAAM,SAAS,KAAU,IAAI;GAI7B,IAAI,WAAW,KAAA,GAAW,OAAO;GAIjC,OAAO,KAAK,UACV,SACC,MAAM,UAAU;IACf,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,GACrD,OAAO,OAAO,MAAM,KAAK,IAAI,SAAS,QAAQ,IAAI,SAAS;IAE7D,OAAO;GACT,GACA,CACF;EACF,SAAS,KAAK;GACZ,OAAO,yBAAyB,QAAQ,GAAG,IAAI,IAAI,UAAU,OAAO,GAAG;EACzE;CACF;AACF,CAAC;;;;;;;;;;AAWD,IAAa,cAAc,IAAI,KAAK;CAClC,MAAM;CACN,aACE;CACF,aAAa,UAAU,OAAO;EAC5B,MAAM,UAAU,OAAO,EAAE,SAAS,EAAE,YAAY,iCAAiC;EACjF,cAAc,UACX,OAAO,EACP,MAAM,KAAK,KAAK,MAAM,EACtB,QAAQ,MAAM,EACd,YAAY,wDAAwD;EACvE,gBAAgB,UACb,OAAO,EACP,MAAM,WAAW,SAAS,aAAa,WAAW,EAClD,QAAQ,SAAS,EACjB,YAAY,4CAA4C;EAC3D,eAAe,UACZ,QAAQ,EACR,QAAQ,IAAI,EACZ,YAAY,4CAA4C;CAC7D,CAAC;CACD,2BAA2B;CAC3B,SAAS,OAAO,SAAS;EACvB,MAAM,EACJ,MACA,cAAc,SACd,gBAAgB,cAChB,eAAe,iBACb;EAaJ,MAAM,YAAY;GALhB,SAAS;GACT,OAAO;GACP,WAAW;GACX,WAAW;EAEK,EAAa,iBAAiB;EAEhD,MAAM,SAAiC,CAAC;EAExC,KAAK,MAAM,OAAO,KAAK,MAAM,SAAS,GAAG;GACvC,MAAM,OAAO,IAAI,KAAK;GACtB,IAAI,SAAS,IAAI;GACjB,IAAI,gBAAgB,KAAK,WAAW,GAAG,GAAG;GAE1C,MAAM,QACJ,YAAY,SAAU,KAAK,SAAS,GAAG,IAAI,MAAM,KAAK,SAAS,GAAG,IAAI,MAAM,OAAQ;GACtF,IAAI,CAAC,OAAO;GAEZ,MAAM,SAAS,KAAK,QAAQ,KAAK;GACjC,IAAI,WAAW,IAAI;GAEnB,MAAM,MAAM,KAAK,MAAM,GAAG,MAAM,EAAE,KAAK;GACvC,IAAI,QAAQ,KAAK,MAAM,SAAS,MAAM,MAAM,EAAE,KAAK;GAEnD,IACG,MAAM,WAAW,IAAG,KAAK,MAAM,SAAS,IAAG,KAC3C,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,GAE5C,QAAQ,MAAM,MAAM,GAAG,EAAE;GAG3B,IAAI,KAAK,OAAO,OAAO;EACzB;EAEA,OAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;CACvC;AACF,CAAC;;;;;;;;;AAUD,IAAa,sBAAsB,IAAI,KAAK;CAC1C,MAAM;CACN,aAAa;CACb,aAAa,UAAU,OAAO,EAC5B,MAAM,UAAU,OAAO,EAAE,SAAS,EAAE,YAAY,yCAAyC,EAC3F,CAAC;CACD,SAAS,OAAO,SAAS;EACvB,MAAM,EAAE,SAAS;EAEjB,MAAM,QADS,KAAK,MAAM,GAAG,GACf,EACX,MAAM,IAAI,EACV,KAAK,MAAM,EAAE,KAAK,CAAC,EACnB,QAAQ,MAAM,EAAE,SAAS,CAAC,EAC1B,MAAM,GAAG,EAAE;EAEd,IAAI,MAAM,WAAW,GAAG,OAAO;EAE/B,MAAM,aAAqD;GACzD;IAAE,OAAO;IAAK,MAAM;GAAQ;GAC5B;IAAE,OAAO;IAAM,MAAM;GAAM;GAC3B;IAAE,OAAO;IAAK,MAAM;GAAY;GAChC;IAAE,OAAO;IAAK,MAAM;GAAO;GAC3B;IAAE,OAAO;IAAK,MAAM;GAAQ;EAC9B;EAEA,IAAI,OAAO;GAAE,MAAM;GAAS,OAAO;GAAK,OAAO;GAAI,WAAW;EAAE;EAEhE,KAAK,MAAM,EAAE,OAAO,UAAU,YAAY;GACxC,MAAM,SAAS,MAAM,KAAK,MAAM,EAAE,MAAM,KAAK,EAAE,SAAS,CAAC;GACzD,MAAM,MAAM,OAAO,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,OAAO;GACvD,IAAI,QAAQ,GAAG;GAEf,MAAM,QAAQ,OAAO,IADC,OAAO,QAAQ,GAAG,MAAM,KAAK,IAAI,QAAQ,GAAG,CAAC,IAAI,OAAO;GAE9E,IAAI,QAAQ,KAAK,OACf,OAAO;IAAE;IAAM;IAAO;IAAO,WAAW,OAAO,YAAY,MAAM,GAAG,QAAQ,CAAC,CAAC;GAAE;EAEpF;EAEA,OAAO,uBAAuB,KAAK,KAAK,KAAK,KAAK,UAAU,MAAO,QAAQ,KAAK,MAAM,QAAQ,KAAK,UAAU;CAC/G;AACF,CAAC"}