@warlock.js/ai-tools 5.30.0 → 5.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +25 -13
  3. package/cjs/index.cjs +968 -122
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/contracts/http.type.d.mts +4 -5
  6. package/esm/contracts/http.type.d.mts.map +1 -1
  7. package/esm/contracts/index.d.mts +3 -1
  8. package/esm/contracts/mcp-serve.type.d.mts +285 -0
  9. package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
  10. package/esm/contracts/mcp.type.d.mts +44 -77
  11. package/esm/contracts/mcp.type.d.mts.map +1 -1
  12. package/esm/contracts/utility.type.d.mts +7 -8
  13. package/esm/contracts/utility.type.d.mts.map +1 -1
  14. package/esm/contracts/web.type.d.mts +9 -10
  15. package/esm/contracts/web.type.d.mts.map +1 -1
  16. package/esm/errors.d.mts +62 -22
  17. package/esm/errors.d.mts.map +1 -1
  18. package/esm/errors.mjs +20 -1
  19. package/esm/errors.mjs.map +1 -1
  20. package/esm/http/http-request.d.mts +2 -3
  21. package/esm/http/http-request.d.mts.map +1 -1
  22. package/esm/http/http-request.mjs +7 -5
  23. package/esm/http/http-request.mjs.map +1 -1
  24. package/esm/index.d.mts +10 -4
  25. package/esm/index.mjs +4 -2
  26. package/esm/mcp/client.mjs +37 -6
  27. package/esm/mcp/client.mjs.map +1 -1
  28. package/esm/mcp/index.d.mts +17 -9
  29. package/esm/mcp/index.d.mts.map +1 -1
  30. package/esm/mcp/index.mjs +3 -1
  31. package/esm/mcp/index.mjs.map +1 -1
  32. package/esm/mcp/json-schema-to-standard.d.mts +1 -3
  33. package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
  34. package/esm/mcp/protocol.d.mts +15 -0
  35. package/esm/mcp/protocol.d.mts.map +1 -0
  36. package/esm/mcp/protocol.mjs +23 -0
  37. package/esm/mcp/protocol.mjs.map +1 -0
  38. package/esm/mcp/result.d.mts +17 -0
  39. package/esm/mcp/result.d.mts.map +1 -0
  40. package/esm/mcp/result.mjs +85 -0
  41. package/esm/mcp/result.mjs.map +1 -0
  42. package/esm/mcp/serve-http.mjs +444 -0
  43. package/esm/mcp/serve-http.mjs.map +1 -0
  44. package/esm/mcp/serve.d.mts +46 -27
  45. package/esm/mcp/serve.d.mts.map +1 -1
  46. package/esm/mcp/serve.mjs +339 -96
  47. package/esm/mcp/serve.mjs.map +1 -1
  48. package/esm/mcp/transport.d.mts +4 -5
  49. package/esm/mcp/transport.d.mts.map +1 -1
  50. package/esm/mcp/transport.mjs +12 -6
  51. package/esm/mcp/transport.mjs.map +1 -1
  52. package/esm/mcp/transport.type.d.mts +2 -2
  53. package/esm/mcp/transport.type.d.mts.map +1 -1
  54. package/esm/register.d.mts +15 -5
  55. package/esm/register.d.mts.map +1 -1
  56. package/esm/register.mjs +3 -2
  57. package/esm/register.mjs.map +1 -1
  58. package/esm/schema.mjs.map +1 -1
  59. package/esm/utility/calculator.d.mts +1 -3
  60. package/esm/utility/calculator.d.mts.map +1 -1
  61. package/esm/utility/calculator.mjs.map +1 -1
  62. package/esm/utility/date-time.d.mts +3 -5
  63. package/esm/utility/date-time.d.mts.map +1 -1
  64. package/esm/utility/date-time.mjs +2 -1
  65. package/esm/utility/date-time.mjs.map +1 -1
  66. package/esm/utility/index.d.mts +2 -1
  67. package/esm/utility/index.mjs +1 -1
  68. package/esm/utility/schema.mjs.map +1 -1
  69. package/esm/web/fetch-url.d.mts +2 -3
  70. package/esm/web/fetch-url.d.mts.map +1 -1
  71. package/esm/web/fetch-url.mjs +2 -1
  72. package/esm/web/fetch-url.mjs.map +1 -1
  73. package/esm/web/index.d.mts +2 -1
  74. package/esm/web/index.mjs +1 -1
  75. package/esm/web/schema.mjs.map +1 -1
  76. package/esm/web/web-search.d.mts +2 -3
  77. package/esm/web/web-search.d.mts.map +1 -1
  78. package/esm/web/web-search.mjs +3 -4
  79. package/esm/web/web-search.mjs.map +1 -1
  80. package/llms-full.txt +245 -56
  81. package/llms.txt +3 -3
  82. package/package.json +2 -6
  83. package/skills/connect-mcp-server/SKILL.md +28 -6
  84. package/skills/expose-as-mcp-server/SKILL.md +168 -35
  85. package/skills/index.md +5 -2
  86. package/skills/use-web-and-http-tools/SKILL.md +49 -15
@@ -1 +1 @@
1
- {"version":3,"file":"calculator.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/utility/calculator.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { CalculatorError } from \"../errors\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type {\n CalculatorInput,\n CalculatorOptions,\n CalculatorResult,\n} from \"../contracts/utility.type\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"calculator\";\n\n/** Input schema for the `calculator` tool: a single `expression` string. */\nconst inputSchema = objectSchema<CalculatorInput>({\n expression: stringField(),\n});\n\n/**\n * A binary operator the evaluator understands, with its precedence and\n * associativity. Higher `precedence` binds tighter; `^` is the only\n * right-associative operator (so `2 ^ 3 ^ 2` is `2 ^ (3 ^ 2)`).\n */\ninterface OperatorSpec {\n precedence: number;\n associativity: \"left\" | \"right\";\n /** Apply the operator to its two operands. May throw {@link CalculatorError}. */\n apply(left: number, right: number): number;\n}\n\n/** The closed set of supported binary operators. */\nconst OPERATORS: Record<string, OperatorSpec> = {\n \"+\": { precedence: 1, associativity: \"left\", apply: (a, b) => a + b },\n \"-\": { precedence: 1, associativity: \"left\", apply: (a, b) => a - b },\n \"*\": { precedence: 2, associativity: \"left\", apply: (a, b) => a * b },\n \"/\": {\n precedence: 2,\n associativity: \"left\",\n apply: (a, b) => {\n if (b === 0) {\n throw new CalculatorError(\"Division by zero.\", { type: \"divide-by-zero\" });\n }\n\n return a / b;\n },\n },\n \"%\": {\n precedence: 2,\n associativity: \"left\",\n apply: (a, b) => {\n if (b === 0) {\n throw new CalculatorError(\"Modulo by zero.\", { type: \"divide-by-zero\" });\n }\n\n return a % b;\n },\n },\n \"^\": { precedence: 3, associativity: \"right\", apply: (a, b) => a ** b },\n};\n\n/** A lexed token: a number literal, an operator, or a parenthesis. */\ntype Token =\n | { type: \"number\"; value: number }\n | { type: \"operator\"; value: string }\n | { type: \"paren\"; value: \"(\" | \")\" };\n\n/**\n * Tokenize an arithmetic expression into {@link Token}s. Recognizes\n * decimal and scientific-notation numbers (`3`, `4.5`, `1e3`, `2.5E-2`),\n * the operators in {@link OPERATORS}, and parentheses; whitespace is\n * skipped. Any other character is a syntax error — there is no path to\n * an identifier, function call, or property access, so nothing\n * code-like can be smuggled in.\n *\n * @throws CalculatorError `type: \"syntax\"` on an unrecognized character\n * or a malformed number.\n */\nfunction tokenize(expression: string): Token[] {\n const tokens: Token[] = [];\n let index = 0;\n\n while (index < expression.length) {\n const char = expression[index];\n\n if (char === \" \" || char === \"\\t\" || char === \"\\n\" || char === \"\\r\") {\n index += 1;\n\n continue;\n }\n\n if (char === \"(\" || char === \")\") {\n tokens.push({ type: \"paren\", value: char });\n index += 1;\n\n continue;\n }\n\n if (char in OPERATORS) {\n tokens.push({ type: \"operator\", value: char });\n index += 1;\n\n continue;\n }\n\n if (isDigit(char) || char === \".\") {\n const { value, nextIndex } = readNumber(expression, index);\n tokens.push({ type: \"number\", value });\n index = nextIndex;\n\n continue;\n }\n\n throw new CalculatorError(\n `Unexpected character \"${char}\" at position ${index}. Only numbers, parentheses, and the operators + - * / % ^ are allowed.`,\n { type: \"syntax\" },\n );\n }\n\n return tokens;\n}\n\n/** True for an ASCII digit `0`–`9`. */\nfunction isDigit(char: string): boolean {\n return char >= \"0\" && char <= \"9\";\n}\n\n/**\n * Read a single number literal starting at `start`. Consumes an optional\n * integer part, optional fraction, and optional exponent\n * (`e`/`E` with an optional sign). Returns the parsed value and the index\n * just past the literal.\n *\n * @throws CalculatorError `type: \"syntax\"` if the consumed run is not a\n * valid finite number (e.g. a lone `.` or `1e` with no exponent).\n */\nfunction readNumber(\n expression: string,\n start: number,\n): { value: number; nextIndex: number } {\n let index = start;\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n\n if (expression[index] === \".\") {\n index += 1;\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n }\n\n if (expression[index] === \"e\" || expression[index] === \"E\") {\n index += 1;\n\n if (expression[index] === \"+\" || expression[index] === \"-\") {\n index += 1;\n }\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n }\n\n const literal = expression.slice(start, index);\n const value = Number(literal);\n\n if (!Number.isFinite(value)) {\n throw new CalculatorError(`Invalid number literal \"${literal}\".`, {\n type: \"syntax\",\n });\n }\n\n return { value, nextIndex: index };\n}\n\n/**\n * Evaluate a token stream with a single left-to-right pass that resolves\n * unary signs, then a shunting-yard conversion that interleaves operator\n * application — so the result is produced without ever building an AST or\n * calling `eval`/`Function`.\n *\n * Unary `+`/`-` are detected positionally: a `+`/`-` is unary when it\n * starts the expression or directly follows another operator or an\n * opening paren. A unary `-` folds into the following number literal\n * (and a unary `+` is a no-op), which keeps the operator stack purely\n * binary.\n *\n * @throws CalculatorError on malformed structure (`type: \"syntax\"`),\n * division/modulo by zero (`type: \"divide-by-zero\"`), or a non-finite\n * result (`type: \"overflow\"`).\n */\nfunction evaluate(tokens: Token[]): number {\n const values: number[] = [];\n const operators: string[] = [];\n\n /** Pop the top operator and apply it to the top two values. */\n const applyTop = (): void => {\n const operator = operators.pop();\n\n if (operator === undefined) {\n throw new CalculatorError(\"Malformed expression.\", { type: \"syntax\" });\n }\n\n const right = values.pop();\n const left = values.pop();\n\n if (left === undefined || right === undefined) {\n throw new CalculatorError(\n `Operator \"${operator}\" is missing an operand.`,\n { type: \"syntax\" },\n );\n }\n\n values.push(OPERATORS[operator].apply(left, right));\n };\n\n // `expectOperand` tracks the positional state machine: at the start, and\n // immediately after an operator or \"(\", we expect an operand (a number,\n // a \"(\", or a unary sign). After a number or \")\", we expect a binary\n // operator or \")\".\n let expectOperand = true;\n // Pending unary sign multiplier folded into the next numeric operand.\n let pendingSign = 1;\n\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i];\n\n if (token.type === \"number\") {\n if (!expectOperand) {\n throw new CalculatorError(\n \"Unexpected number — a number cannot directly follow another value.\",\n { type: \"syntax\" },\n );\n }\n\n values.push(pendingSign * token.value);\n pendingSign = 1;\n expectOperand = false;\n\n continue;\n }\n\n if (token.type === \"paren\") {\n if (token.value === \"(\") {\n if (!expectOperand) {\n throw new CalculatorError(\n 'Unexpected \"(\" — it cannot directly follow a value.',\n { type: \"syntax\" },\n );\n }\n\n // A unary sign in front of a parenthesized group is modeled by\n // pushing the sign as a multiplication: `-(…)` becomes `-1 * (…)`.\n if (pendingSign === -1) {\n values.push(-1);\n operators.push(\"*\");\n pendingSign = 1;\n }\n\n operators.push(\"(\");\n expectOperand = true;\n\n continue;\n }\n\n // token.value === \")\"\n if (expectOperand) {\n throw new CalculatorError(\n 'Unexpected \")\" — an operand was expected.',\n { type: \"syntax\" },\n );\n }\n\n let foundOpen = false;\n\n while (operators.length > 0) {\n if (operators[operators.length - 1] === \"(\") {\n operators.pop();\n foundOpen = true;\n\n break;\n }\n\n applyTop();\n }\n\n if (!foundOpen) {\n throw new CalculatorError(\"Unbalanced parentheses.\", { type: \"syntax\" });\n }\n\n expectOperand = false;\n\n continue;\n }\n\n // token.type === \"operator\"\n if (expectOperand) {\n // A `+`/`-` in operand position is a unary sign; anything else is a\n // misplaced binary operator.\n if (token.value === \"-\") {\n pendingSign = -pendingSign;\n\n continue;\n }\n\n if (token.value === \"+\") {\n continue;\n }\n\n throw new CalculatorError(\n `Operator \"${token.value}\" has no left-hand operand.`,\n { type: \"syntax\" },\n );\n }\n\n const incoming = OPERATORS[token.value];\n\n while (operators.length > 0) {\n const top = operators[operators.length - 1];\n\n if (top === \"(\") {\n break;\n }\n\n const topSpec = OPERATORS[top];\n const higher = topSpec.precedence > incoming.precedence;\n const equalLeft =\n topSpec.precedence === incoming.precedence &&\n incoming.associativity === \"left\";\n\n if (higher || equalLeft) {\n applyTop();\n\n continue;\n }\n\n break;\n }\n\n operators.push(token.value);\n expectOperand = true;\n }\n\n if (expectOperand) {\n throw new CalculatorError(\n \"Expression ends with an operator or is empty.\",\n { type: \"syntax\" },\n );\n }\n\n while (operators.length > 0) {\n if (operators[operators.length - 1] === \"(\") {\n throw new CalculatorError(\"Unbalanced parentheses.\", { type: \"syntax\" });\n }\n\n applyTop();\n }\n\n const result = values.pop();\n\n if (result === undefined || values.length > 0) {\n throw new CalculatorError(\"Malformed expression.\", { type: \"syntax\" });\n }\n\n if (!Number.isFinite(result)) {\n throw new CalculatorError(\"Result is not a finite number.\", {\n type: \"overflow\",\n });\n }\n\n return result;\n}\n\n/**\n * Build the `calculator` tool — a SAFE arithmetic evaluator the agent can\n * call to compute a numeric expression. It supports `+ - * / % ^`, unary\n * signs, parentheses, and decimal/scientific-notation literals, with the\n * usual precedence (`^` highest and right-associative, then `* / %`, then\n * `+ -`).\n *\n * **Safety.** The expression is tokenized and evaluated with a\n * shunting-yard pass — it NEVER calls `eval` or `new Function`. The\n * lexer only recognizes numbers, parentheses, and the fixed operator set,\n * so there is no path to an identifier, function call, or property\n * access; any other character is a syntax error returned as data.\n *\n * **Errors flow as data.** A malformed expression, division/modulo by\n * zero, or a non-finite result throws a {@link CalculatorError} inside\n * the handler; `tool()` catches it and surfaces it in the returned\n * `{ error }` field (the LLM-visible message is preserved), so the agent\n * reads the failure and self-corrects instead of crashing.\n *\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n * @returns A `ToolContract<{ expression }, { result }>` ready to drop into `tools: []`.\n *\n * @example\n * const calc = calculatorTool();\n * const { data } = await calc.invoke({ expression: \"(3 + 4) * 2\" });\n * console.log(data?.result); // 14\n */\nexport function calculatorTool(\n options?: CalculatorOptions,\n): ToolContract<CalculatorInput, CalculatorResult> {\n return tool<CalculatorInput, CalculatorResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Evaluate an arithmetic expression and return the numeric result. \" +\n \"Supports + - * / % ^, parentheses, unary +/-, and decimal or \" +\n \"scientific-notation numbers. Does not support variables or \" +\n \"functions — pass a literal expression like \\\"(3 + 4) * 2\\\".\",\n input: inputSchema,\n async execute(input) {\n const tokens = tokenize(input.expression);\n\n if (tokens.length === 0) {\n throw new CalculatorError(\"Expression is empty.\", { type: \"syntax\" });\n }\n\n return { result: evaluate(tokens) };\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"calculator.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/utility/calculator.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { CalculatorError } from \"../errors\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type {\n CalculatorInput,\n CalculatorOptions,\n CalculatorResult,\n} from \"../contracts/utility.type\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"calculator\";\n\n/** Input schema for the `calculator` tool: a single `expression` string. */\nconst inputSchema = objectSchema<CalculatorInput>({\n expression: stringField(),\n});\n\n/**\n * A binary operator the evaluator understands, with its precedence and\n * associativity. Higher `precedence` binds tighter; `^` is the only\n * right-associative operator (so `2 ^ 3 ^ 2` is `2 ^ (3 ^ 2)`).\n */\ninterface OperatorSpec {\n precedence: number;\n associativity: \"left\" | \"right\";\n /** Apply the operator to its two operands. May throw {@link CalculatorError}. */\n apply(left: number, right: number): number;\n}\n\n/** The closed set of supported binary operators. */\nconst OPERATORS: Record<string, OperatorSpec> = {\n \"+\": { precedence: 1, associativity: \"left\", apply: (a, b) => a + b },\n \"-\": { precedence: 1, associativity: \"left\", apply: (a, b) => a - b },\n \"*\": { precedence: 2, associativity: \"left\", apply: (a, b) => a * b },\n \"/\": {\n precedence: 2,\n associativity: \"left\",\n apply: (a, b) => {\n if (b === 0) {\n throw new CalculatorError(\"Division by zero.\", { type: \"divide-by-zero\" });\n }\n\n return a / b;\n },\n },\n \"%\": {\n precedence: 2,\n associativity: \"left\",\n apply: (a, b) => {\n if (b === 0) {\n throw new CalculatorError(\"Modulo by zero.\", { type: \"divide-by-zero\" });\n }\n\n return a % b;\n },\n },\n \"^\": { precedence: 3, associativity: \"right\", apply: (a, b) => a ** b },\n};\n\n/** A lexed token: a number literal, an operator, or a parenthesis. */\ntype Token =\n | { type: \"number\"; value: number }\n | { type: \"operator\"; value: string }\n | { type: \"paren\"; value: \"(\" | \")\" };\n\n/**\n * Tokenize an arithmetic expression into {@link Token}s. Recognizes\n * decimal and scientific-notation numbers (`3`, `4.5`, `1e3`, `2.5E-2`),\n * the operators in {@link OPERATORS}, and parentheses; whitespace is\n * skipped. Any other character is a syntax error — there is no path to\n * an identifier, function call, or property access, so nothing\n * code-like can be smuggled in.\n *\n * @throws CalculatorError `type: \"syntax\"` on an unrecognized character\n * or a malformed number.\n */\nfunction tokenize(expression: string): Token[] {\n const tokens: Token[] = [];\n let index = 0;\n\n while (index < expression.length) {\n const char = expression[index];\n\n if (char === \" \" || char === \"\\t\" || char === \"\\n\" || char === \"\\r\") {\n index += 1;\n\n continue;\n }\n\n if (char === \"(\" || char === \")\") {\n tokens.push({ type: \"paren\", value: char });\n index += 1;\n\n continue;\n }\n\n if (char in OPERATORS) {\n tokens.push({ type: \"operator\", value: char });\n index += 1;\n\n continue;\n }\n\n if (isDigit(char) || char === \".\") {\n const { value, nextIndex } = readNumber(expression, index);\n tokens.push({ type: \"number\", value });\n index = nextIndex;\n\n continue;\n }\n\n throw new CalculatorError(\n `Unexpected character \"${char}\" at position ${index}. Only numbers, parentheses, and the operators + - * / % ^ are allowed.`,\n { type: \"syntax\" },\n );\n }\n\n return tokens;\n}\n\n/** True for an ASCII digit `0`–`9`. */\nfunction isDigit(char: string): boolean {\n return char >= \"0\" && char <= \"9\";\n}\n\n/**\n * Read a single number literal starting at `start`. Consumes an optional\n * integer part, optional fraction, and optional exponent\n * (`e`/`E` with an optional sign). Returns the parsed value and the index\n * just past the literal.\n *\n * @throws CalculatorError `type: \"syntax\"` if the consumed run is not a\n * valid finite number (e.g. a lone `.` or `1e` with no exponent).\n */\nfunction readNumber(expression: string, start: number): { value: number; nextIndex: number } {\n let index = start;\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n\n if (expression[index] === \".\") {\n index += 1;\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n }\n\n if (expression[index] === \"e\" || expression[index] === \"E\") {\n index += 1;\n\n if (expression[index] === \"+\" || expression[index] === \"-\") {\n index += 1;\n }\n\n while (index < expression.length && isDigit(expression[index])) {\n index += 1;\n }\n }\n\n const literal = expression.slice(start, index);\n const value = Number(literal);\n\n if (!Number.isFinite(value)) {\n throw new CalculatorError(`Invalid number literal \"${literal}\".`, {\n type: \"syntax\",\n });\n }\n\n return { value, nextIndex: index };\n}\n\n/**\n * Evaluate a token stream with a single left-to-right pass that resolves\n * unary signs, then a shunting-yard conversion that interleaves operator\n * application — so the result is produced without ever building an AST or\n * calling `eval`/`Function`.\n *\n * Unary `+`/`-` are detected positionally: a `+`/`-` is unary when it\n * starts the expression or directly follows another operator or an\n * opening paren. A unary `-` folds into the following number literal\n * (and a unary `+` is a no-op), which keeps the operator stack purely\n * binary.\n *\n * @throws CalculatorError on malformed structure (`type: \"syntax\"`),\n * division/modulo by zero (`type: \"divide-by-zero\"`), or a non-finite\n * result (`type: \"overflow\"`).\n */\nfunction evaluate(tokens: Token[]): number {\n const values: number[] = [];\n const operators: string[] = [];\n\n /** Pop the top operator and apply it to the top two values. */\n const applyTop = (): void => {\n const operator = operators.pop();\n\n if (operator === undefined) {\n throw new CalculatorError(\"Malformed expression.\", { type: \"syntax\" });\n }\n\n const right = values.pop();\n const left = values.pop();\n\n if (left === undefined || right === undefined) {\n throw new CalculatorError(`Operator \"${operator}\" is missing an operand.`, {\n type: \"syntax\",\n });\n }\n\n values.push(OPERATORS[operator].apply(left, right));\n };\n\n // `expectOperand` tracks the positional state machine: at the start, and\n // immediately after an operator or \"(\", we expect an operand (a number,\n // a \"(\", or a unary sign). After a number or \")\", we expect a binary\n // operator or \")\".\n let expectOperand = true;\n // Pending unary sign multiplier folded into the next numeric operand.\n let pendingSign = 1;\n\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i];\n\n if (token.type === \"number\") {\n if (!expectOperand) {\n throw new CalculatorError(\n \"Unexpected number — a number cannot directly follow another value.\",\n { type: \"syntax\" },\n );\n }\n\n values.push(pendingSign * token.value);\n pendingSign = 1;\n expectOperand = false;\n\n continue;\n }\n\n if (token.type === \"paren\") {\n if (token.value === \"(\") {\n if (!expectOperand) {\n throw new CalculatorError('Unexpected \"(\" — it cannot directly follow a value.', {\n type: \"syntax\",\n });\n }\n\n // A unary sign in front of a parenthesized group is modeled by\n // pushing the sign as a multiplication: `-(…)` becomes `-1 * (…)`.\n if (pendingSign === -1) {\n values.push(-1);\n operators.push(\"*\");\n pendingSign = 1;\n }\n\n operators.push(\"(\");\n expectOperand = true;\n\n continue;\n }\n\n // token.value === \")\"\n if (expectOperand) {\n throw new CalculatorError('Unexpected \")\" — an operand was expected.', { type: \"syntax\" });\n }\n\n let foundOpen = false;\n\n while (operators.length > 0) {\n if (operators[operators.length - 1] === \"(\") {\n operators.pop();\n foundOpen = true;\n\n break;\n }\n\n applyTop();\n }\n\n if (!foundOpen) {\n throw new CalculatorError(\"Unbalanced parentheses.\", { type: \"syntax\" });\n }\n\n expectOperand = false;\n\n continue;\n }\n\n // token.type === \"operator\"\n if (expectOperand) {\n // A `+`/`-` in operand position is a unary sign; anything else is a\n // misplaced binary operator.\n if (token.value === \"-\") {\n pendingSign = -pendingSign;\n\n continue;\n }\n\n if (token.value === \"+\") {\n continue;\n }\n\n throw new CalculatorError(`Operator \"${token.value}\" has no left-hand operand.`, {\n type: \"syntax\",\n });\n }\n\n const incoming = OPERATORS[token.value];\n\n while (operators.length > 0) {\n const top = operators[operators.length - 1];\n\n if (top === \"(\") {\n break;\n }\n\n const topSpec = OPERATORS[top];\n const higher = topSpec.precedence > incoming.precedence;\n const equalLeft =\n topSpec.precedence === incoming.precedence && incoming.associativity === \"left\";\n\n if (higher || equalLeft) {\n applyTop();\n\n continue;\n }\n\n break;\n }\n\n operators.push(token.value);\n expectOperand = true;\n }\n\n if (expectOperand) {\n throw new CalculatorError(\"Expression ends with an operator or is empty.\", { type: \"syntax\" });\n }\n\n while (operators.length > 0) {\n if (operators[operators.length - 1] === \"(\") {\n throw new CalculatorError(\"Unbalanced parentheses.\", { type: \"syntax\" });\n }\n\n applyTop();\n }\n\n const result = values.pop();\n\n if (result === undefined || values.length > 0) {\n throw new CalculatorError(\"Malformed expression.\", { type: \"syntax\" });\n }\n\n if (!Number.isFinite(result)) {\n throw new CalculatorError(\"Result is not a finite number.\", {\n type: \"overflow\",\n });\n }\n\n return result;\n}\n\n/**\n * Build the `calculator` tool — a SAFE arithmetic evaluator the agent can\n * call to compute a numeric expression. It supports `+ - * / % ^`, unary\n * signs, parentheses, and decimal/scientific-notation literals, with the\n * usual precedence (`^` highest and right-associative, then `* / %`, then\n * `+ -`).\n *\n * **Safety.** The expression is tokenized and evaluated with a\n * shunting-yard pass — it NEVER calls `eval` or `new Function`. The\n * lexer only recognizes numbers, parentheses, and the fixed operator set,\n * so there is no path to an identifier, function call, or property\n * access; any other character is a syntax error returned as data.\n *\n * **Errors flow as data.** A malformed expression, division/modulo by\n * zero, or a non-finite result throws a {@link CalculatorError} inside\n * the handler; `tool()` catches it and surfaces it in the returned\n * `{ error }` field (the LLM-visible message is preserved), so the agent\n * reads the failure and self-corrects instead of crashing.\n *\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n * @returns A `ToolContract<{ expression }, { result }>` ready to drop into `tools: []`.\n *\n * @example\n * const calc = calculatorTool();\n * const { data } = await calc.invoke({ expression: \"(3 + 4) * 2\" });\n * console.log(data?.result); // 14\n */\nexport function calculatorTool(\n options?: CalculatorOptions,\n): ToolContract<CalculatorInput, CalculatorResult> {\n return tool<CalculatorInput, CalculatorResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Evaluate an arithmetic expression and return the numeric result. \" +\n \"Supports + - * / % ^, parentheses, unary +/-, and decimal or \" +\n \"scientific-notation numbers. Does not support variables or \" +\n 'functions — pass a literal expression like \"(3 + 4) * 2\".',\n input: inputSchema,\n async execute(input) {\n const tokens = tokenize(input.expression);\n\n if (tokens.length === 0) {\n throw new CalculatorError(\"Expression is empty.\", { type: \"syntax\" });\n }\n\n return { result: evaluate(tokens) };\n },\n });\n}\n"],"mappings":"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"}
@@ -1,15 +1,14 @@
1
1
  import { DateTimeInput, DateTimeOptions, DateTimeResult } from "../contracts/utility.type.mjs";
2
2
  import { ToolContract } from "@warlock.js/ai";
3
-
4
3
  //#region ../../packages/ai-tools/src/utility/date-time.d.ts
5
4
  /**
6
5
  * A clock seam so tests are deterministic. Defaults to the real
7
6
  * {@link Date}; a test injects a fixed instant. Returns milliseconds
8
7
  * since the Unix epoch — the same contract as `Date.now()`.
9
8
  */
10
- type Clock = () => number;
9
+ export type Clock = () => number;
11
10
  /** Construction dependencies for {@link dateTimeTool}, all optional. */
12
- interface DateTimeToolDeps {
11
+ export interface DateTimeToolDeps {
13
12
  /**
14
13
  * The clock used by the `now` op. Defaults to `Date.now`. Injected in
15
14
  * tests for deterministic output.
@@ -51,7 +50,6 @@ interface DateTimeToolDeps {
51
50
  * const { data } = await dt.invoke({ op: "now" });
52
51
  * console.log(data?.value); // "2026-06-22T00:00:00.000Z"
53
52
  */
54
- declare function dateTimeTool(options?: DateTimeOptions, deps?: DateTimeToolDeps): ToolContract<DateTimeInput, DateTimeResult>;
53
+ export declare function dateTimeTool(options?: DateTimeOptions, deps?: DateTimeToolDeps): ToolContract<DateTimeInput, DateTimeResult>;
55
54
  //#endregion
56
- export { Clock, DateTimeToolDeps, dateTimeTool };
57
55
  //# sourceMappingURL=date-time.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"date-time.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/utility/date-time.ts"],"mappings":";;;;;;AAqBA;;;KAAY,KAAA;AAAK;AAAA,UAGA,gBAAA;EAAgB;;;AAKlB;EAAb,KAAA,GAAQ,KAAK;AAAA;;;;;;;;;;;;;;;;;AAqL8B;;;;;;;;;;;;;;;;;;;iBAH7B,YAAA,CACd,OAAA,GAAU,eAAA,EACV,IAAA,GAAO,gBAAA,GACN,YAAA,CAAa,aAAA,EAAe,cAAA"}
1
+ {"version":3,"file":"date-time.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/utility/date-time.ts"],"mappings":";;;;;;;;YAqBY;;iBAGK;;;;;EAKf,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgLM,aACd,UAAU,iBACV,OAAO,mBACN,aAAa,eAAe"}
@@ -55,7 +55,8 @@ const UNIT_ALIASES = {
55
55
  };
56
56
  /** Resolve a (possibly aliased / singular) unit token to its ms length. */
57
57
  function unitToMs(unit) {
58
- const ms = UNIT_MS[UNIT_ALIASES[unit] ?? unit];
58
+ const canonical = UNIT_ALIASES[unit] ?? unit;
59
+ const ms = UNIT_MS[canonical];
59
60
  if (ms === void 0) throw new DateTimeError(`Unknown unit "${unit}". Supported units: ${Object.keys(UNIT_MS).join(", ")}.`, { type: "invalid-unit" });
60
61
  return ms;
61
62
  }
@@ -1 +1 @@
1
- {"version":3,"file":"date-time.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/utility/date-time.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { DateTimeError } from \"../errors\";\nimport { literalField, objectSchema, optionalNumberField, optionalStringField } from \"./schema\";\nimport type {\n DateTimeInput,\n DateTimeOp,\n DateTimeOptions,\n DateTimeResult,\n} from \"../contracts/utility.type\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"date_time\";\n\n/** The closed set of operations the tool implements. */\nconst OPS: readonly DateTimeOp[] = [\"now\", \"add\", \"diff\", \"format\"];\n\n/**\n * A clock seam so tests are deterministic. Defaults to the real\n * {@link Date}; a test injects a fixed instant. Returns milliseconds\n * since the Unix epoch — the same contract as `Date.now()`.\n */\nexport type Clock = () => number;\n\n/** Construction dependencies for {@link dateTimeTool}, all optional. */\nexport interface DateTimeToolDeps {\n /**\n * The clock used by the `now` op. Defaults to `Date.now`. Injected in\n * tests for deterministic output.\n */\n clock?: Clock;\n}\n\n/** Input schema for the `date_time` tool. */\nconst inputSchema = objectSchema<DateTimeInput>({\n op: literalField<DateTimeOp>(OPS),\n iso: optionalStringField(),\n from: optionalStringField(),\n to: optionalStringField(),\n amount: optionalNumberField(),\n unit: optionalStringField(),\n timeZone: optionalStringField(),\n format: optionalStringField(),\n});\n\n/**\n * The time units `add` and `diff` understand, each as its length in\n * milliseconds. Calendar-unaware on purpose: a \"month\" / \"year\" has no\n * fixed millisecond length, so they are deliberately excluded — adding\n * `30 days` is unambiguous in a way adding `1 month` is not.\n */\nconst UNIT_MS: Record<string, number> = {\n milliseconds: 1,\n seconds: 1_000,\n minutes: 60_000,\n hours: 3_600_000,\n days: 86_400_000,\n weeks: 604_800_000,\n};\n\n/** Singular aliases mapped to their canonical plural unit key. */\nconst UNIT_ALIASES: Record<string, string> = {\n millisecond: \"milliseconds\",\n ms: \"milliseconds\",\n second: \"seconds\",\n sec: \"seconds\",\n s: \"seconds\",\n minute: \"minutes\",\n min: \"minutes\",\n m: \"minutes\",\n hour: \"hours\",\n hr: \"hours\",\n h: \"hours\",\n day: \"days\",\n d: \"days\",\n week: \"weeks\",\n w: \"weeks\",\n};\n\n/** Resolve a (possibly aliased / singular) unit token to its ms length. */\nfunction unitToMs(unit: string): number {\n const canonical = UNIT_ALIASES[unit] ?? unit;\n const ms = UNIT_MS[canonical];\n\n if (ms === undefined) {\n throw new DateTimeError(\n `Unknown unit \"${unit}\". Supported units: ${Object.keys(UNIT_MS).join(\", \")}.`,\n { type: \"invalid-unit\" },\n );\n }\n\n return ms;\n}\n\n/**\n * Parse an ISO-8601 instant into a {@link Date}, or throw a typed\n * {@link DateTimeError} when the string is missing or unparseable.\n *\n * @param iso - The ISO string from the model (may be undefined).\n * @param field - The input field name, for the error message.\n */\nfunction parseIso(iso: string | undefined, field: string): Date {\n if (iso === undefined) {\n throw new DateTimeError(`\"${field}\" is required for this operation.`, {\n type: \"invalid-input\",\n });\n }\n\n const date = new Date(iso);\n\n if (Number.isNaN(date.getTime())) {\n throw new DateTimeError(`\"${field}\" is not a valid ISO-8601 instant: \"${iso}\".`, {\n type: \"invalid-input\",\n });\n }\n\n return date;\n}\n\n/**\n * Render a {@link Date} in a target time zone using `Intl`. The\n * `\"iso\"` format (the default) returns the instant's UTC ISO string;\n * any other `format` value is treated as an `Intl.DateTimeFormat`\n * locale-style rendering in the given `timeZone`.\n *\n * @throws DateTimeError `type: \"invalid-time-zone\"` when `timeZone` is\n * not a recognized IANA zone.\n */\nfunction render(date: Date, format: string | undefined, timeZone: string | undefined): string {\n if (format === undefined || format === \"iso\") {\n // `timeZone` is irrelevant to a UTC ISO string, but validate it when\n // supplied so a bad zone is reported rather than silently ignored.\n if (timeZone !== undefined) {\n assertTimeZone(timeZone);\n }\n\n return date.toISOString();\n }\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n dateStyle: format === \"date\" ? \"medium\" : undefined,\n timeStyle: format === \"time\" ? \"medium\" : undefined,\n ...(format === \"datetime\" ? { dateStyle: \"medium\", timeStyle: \"medium\" } : {}),\n }).format(date);\n } catch (error) {\n throw new DateTimeError(\n `Could not render with format \"${format}\"${\n timeZone ? ` in time zone \"${timeZone}\"` : \"\"\n }.`,\n { type: \"invalid-time-zone\", cause: error },\n );\n }\n}\n\n/**\n * Validate an IANA time zone by attempting to construct a formatter for\n * it; an unrecognized zone makes `Intl` throw a `RangeError`.\n *\n * @throws DateTimeError `type: \"invalid-time-zone\"` for an unknown zone.\n */\nfunction assertTimeZone(timeZone: string): void {\n try {\n new Intl.DateTimeFormat(\"en-US\", { timeZone });\n } catch (error) {\n throw new DateTimeError(`Unknown time zone \"${timeZone}\".`, {\n type: \"invalid-time-zone\",\n cause: error,\n });\n }\n}\n\n/**\n * Build the `date_time` tool — a small clock/calendar utility the agent\n * can call to read the current instant, shift an instant, measure the\n * gap between two instants, or render one. The `op` discriminator selects\n * the operation:\n *\n * - **`now`** — the current instant, rendered per `format` / `timeZone`\n * (defaults to a UTC ISO string). Reads the injectable {@link Clock}.\n * - **`add`** — `iso` shifted by `amount` of `unit` (e.g.\n * `+3 days`); a negative `amount` shifts backward.\n * - **`diff`** — the signed difference `to − iso`, expressed in `unit`.\n * - **`format`** — `iso` rendered per `format` / `timeZone`.\n *\n * Units are millisecond-based (`milliseconds`…`weeks`, plus common\n * aliases); calendar-relative `month`/`year` are intentionally\n * unsupported because they have no fixed length.\n *\n * **Deterministic in tests.** The clock backing `now` is injectable via\n * `deps.clock`; production defaults to `Date.now`.\n *\n * **Errors flow as data.** A missing/invalid field, an unknown unit, or\n * an unrecognized time zone throws a {@link DateTimeError} inside the\n * handler; `tool()` surfaces it in `{ error }` so the agent self-corrects.\n *\n * @param options - Optional overrides; `name` renames the tool,\n * `defaultTimeZone` applies when a call omits `timeZone`.\n * @param deps - Injectable dependencies (the {@link Clock}); defaults to real time.\n * @returns A `ToolContract<DateTimeInput, { value }>` ready for `tools: []`.\n *\n * @example\n * const clock = () => Date.parse(\"2026-06-22T00:00:00Z\");\n * const dt = dateTimeTool({}, { clock });\n * const { data } = await dt.invoke({ op: \"now\" });\n * console.log(data?.value); // \"2026-06-22T00:00:00.000Z\"\n */\nexport function dateTimeTool(\n options?: DateTimeOptions,\n deps?: DateTimeToolDeps,\n): ToolContract<DateTimeInput, DateTimeResult> {\n const clock: Clock = deps?.clock ?? Date.now;\n const defaultTimeZone = options?.defaultTimeZone;\n\n return tool<DateTimeInput, DateTimeResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Work with dates and times. Set op to: \\\"now\\\" (current instant), \" +\n '\"add\" (shift iso by amount of unit), \"diff\" (signed to − from in unit; ' +\n \"pass the start instant as iso or from), \" +\n 'or \"format\" (render iso). Units are milliseconds, seconds, minutes, ' +\n \"hours, days, or weeks (month/year are not supported). Pass instants as \" +\n 'ISO-8601 strings; set format to \"iso\" (default), \"date\", \"time\", or ' +\n '\"datetime\", and timeZone as an IANA zone like \"Africa/Cairo\".',\n input: inputSchema,\n async execute(input) {\n const timeZone = input.timeZone ?? defaultTimeZone;\n\n switch (input.op) {\n case \"now\": {\n const date = new Date(clock());\n\n return { value: render(date, input.format, timeZone) };\n }\n\n case \"format\": {\n const date = parseIso(input.iso, \"iso\");\n\n return { value: render(date, input.format, timeZone) };\n }\n\n case \"add\": {\n const date = parseIso(input.iso, \"iso\");\n\n if (input.amount === undefined) {\n throw new DateTimeError('\"amount\" is required for the \"add\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n if (input.unit === undefined) {\n throw new DateTimeError('\"unit\" is required for the \"add\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n const shifted = new Date(date.getTime() + input.amount * unitToMs(input.unit));\n\n return { value: render(shifted, input.format, timeZone) };\n }\n\n case \"diff\": {\n // Accept `from` as an alias for `iso` — models naturally pass\n // `from` / `to` for a difference. `iso` wins when both are set.\n const from = parseIso(input.iso ?? input.from, \"iso (or from)\");\n const to = parseIso(input.to, \"to\");\n\n if (input.unit === undefined) {\n throw new DateTimeError('\"unit\" is required for the \"diff\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n const diff = (to.getTime() - from.getTime()) / unitToMs(input.unit);\n\n return { value: String(diff) };\n }\n\n default: {\n // Exhaustiveness guard — the schema's literal union should make\n // this unreachable, but a future op added to the union without a\n // case here surfaces as typed data rather than silent fallthrough.\n const unreachable: never = input.op;\n\n throw new DateTimeError(`Unsupported operation \"${String(unreachable)}\".`, {\n type: \"unsupported-op\",\n });\n }\n }\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"date-time.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/utility/date-time.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { DateTimeError } from \"../errors\";\nimport { literalField, objectSchema, optionalNumberField, optionalStringField } from \"./schema\";\nimport type {\n DateTimeInput,\n DateTimeOp,\n DateTimeOptions,\n DateTimeResult,\n} from \"../contracts/utility.type\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"date_time\";\n\n/** The closed set of operations the tool implements. */\nconst OPS: readonly DateTimeOp[] = [\"now\", \"add\", \"diff\", \"format\"];\n\n/**\n * A clock seam so tests are deterministic. Defaults to the real\n * {@link Date}; a test injects a fixed instant. Returns milliseconds\n * since the Unix epoch — the same contract as `Date.now()`.\n */\nexport type Clock = () => number;\n\n/** Construction dependencies for {@link dateTimeTool}, all optional. */\nexport interface DateTimeToolDeps {\n /**\n * The clock used by the `now` op. Defaults to `Date.now`. Injected in\n * tests for deterministic output.\n */\n clock?: Clock;\n}\n\n/** Input schema for the `date_time` tool. */\nconst inputSchema = objectSchema<DateTimeInput>({\n op: literalField<DateTimeOp>(OPS),\n iso: optionalStringField(),\n from: optionalStringField(),\n to: optionalStringField(),\n amount: optionalNumberField(),\n unit: optionalStringField(),\n timeZone: optionalStringField(),\n format: optionalStringField(),\n});\n\n/**\n * The time units `add` and `diff` understand, each as its length in\n * milliseconds. Calendar-unaware on purpose: a \"month\" / \"year\" has no\n * fixed millisecond length, so they are deliberately excluded — adding\n * `30 days` is unambiguous in a way adding `1 month` is not.\n */\nconst UNIT_MS: Record<string, number> = {\n milliseconds: 1,\n seconds: 1_000,\n minutes: 60_000,\n hours: 3_600_000,\n days: 86_400_000,\n weeks: 604_800_000,\n};\n\n/** Singular aliases mapped to their canonical plural unit key. */\nconst UNIT_ALIASES: Record<string, string> = {\n millisecond: \"milliseconds\",\n ms: \"milliseconds\",\n second: \"seconds\",\n sec: \"seconds\",\n s: \"seconds\",\n minute: \"minutes\",\n min: \"minutes\",\n m: \"minutes\",\n hour: \"hours\",\n hr: \"hours\",\n h: \"hours\",\n day: \"days\",\n d: \"days\",\n week: \"weeks\",\n w: \"weeks\",\n};\n\n/** Resolve a (possibly aliased / singular) unit token to its ms length. */\nfunction unitToMs(unit: string): number {\n const canonical = UNIT_ALIASES[unit] ?? unit;\n const ms = UNIT_MS[canonical];\n\n if (ms === undefined) {\n throw new DateTimeError(\n `Unknown unit \"${unit}\". Supported units: ${Object.keys(UNIT_MS).join(\", \")}.`,\n { type: \"invalid-unit\" },\n );\n }\n\n return ms;\n}\n\n/**\n * Parse an ISO-8601 instant into a {@link Date}, or throw a typed\n * {@link DateTimeError} when the string is missing or unparseable.\n *\n * @param iso - The ISO string from the model (may be undefined).\n * @param field - The input field name, for the error message.\n */\nfunction parseIso(iso: string | undefined, field: string): Date {\n if (iso === undefined) {\n throw new DateTimeError(`\"${field}\" is required for this operation.`, {\n type: \"invalid-input\",\n });\n }\n\n const date = new Date(iso);\n\n if (Number.isNaN(date.getTime())) {\n throw new DateTimeError(`\"${field}\" is not a valid ISO-8601 instant: \"${iso}\".`, {\n type: \"invalid-input\",\n });\n }\n\n return date;\n}\n\n/**\n * Render a {@link Date} in a target time zone using `Intl`. The\n * `\"iso\"` format (the default) returns the instant's UTC ISO string;\n * any other `format` value is treated as an `Intl.DateTimeFormat`\n * locale-style rendering in the given `timeZone`.\n *\n * @throws DateTimeError `type: \"invalid-time-zone\"` when `timeZone` is\n * not a recognized IANA zone.\n */\nfunction render(date: Date, format: string | undefined, timeZone: string | undefined): string {\n if (format === undefined || format === \"iso\") {\n // `timeZone` is irrelevant to a UTC ISO string, but validate it when\n // supplied so a bad zone is reported rather than silently ignored.\n if (timeZone !== undefined) {\n assertTimeZone(timeZone);\n }\n\n return date.toISOString();\n }\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n dateStyle: format === \"date\" ? \"medium\" : undefined,\n timeStyle: format === \"time\" ? \"medium\" : undefined,\n ...(format === \"datetime\" ? { dateStyle: \"medium\", timeStyle: \"medium\" } : {}),\n }).format(date);\n } catch (error) {\n throw new DateTimeError(\n `Could not render with format \"${format}\"${timeZone ? ` in time zone \"${timeZone}\"` : \"\"}.`,\n { type: \"invalid-time-zone\", cause: error },\n );\n }\n}\n\n/**\n * Validate an IANA time zone by attempting to construct a formatter for\n * it; an unrecognized zone makes `Intl` throw a `RangeError`.\n *\n * @throws DateTimeError `type: \"invalid-time-zone\"` for an unknown zone.\n */\nfunction assertTimeZone(timeZone: string): void {\n try {\n new Intl.DateTimeFormat(\"en-US\", { timeZone });\n } catch (error) {\n throw new DateTimeError(`Unknown time zone \"${timeZone}\".`, {\n type: \"invalid-time-zone\",\n cause: error,\n });\n }\n}\n\n/**\n * Build the `date_time` tool — a small clock/calendar utility the agent\n * can call to read the current instant, shift an instant, measure the\n * gap between two instants, or render one. The `op` discriminator selects\n * the operation:\n *\n * - **`now`** — the current instant, rendered per `format` / `timeZone`\n * (defaults to a UTC ISO string). Reads the injectable {@link Clock}.\n * - **`add`** — `iso` shifted by `amount` of `unit` (e.g.\n * `+3 days`); a negative `amount` shifts backward.\n * - **`diff`** — the signed difference `to − iso`, expressed in `unit`.\n * - **`format`** — `iso` rendered per `format` / `timeZone`.\n *\n * Units are millisecond-based (`milliseconds`…`weeks`, plus common\n * aliases); calendar-relative `month`/`year` are intentionally\n * unsupported because they have no fixed length.\n *\n * **Deterministic in tests.** The clock backing `now` is injectable via\n * `deps.clock`; production defaults to `Date.now`.\n *\n * **Errors flow as data.** A missing/invalid field, an unknown unit, or\n * an unrecognized time zone throws a {@link DateTimeError} inside the\n * handler; `tool()` surfaces it in `{ error }` so the agent self-corrects.\n *\n * @param options - Optional overrides; `name` renames the tool,\n * `defaultTimeZone` applies when a call omits `timeZone`.\n * @param deps - Injectable dependencies (the {@link Clock}); defaults to real time.\n * @returns A `ToolContract<DateTimeInput, { value }>` ready for `tools: []`.\n *\n * @example\n * const clock = () => Date.parse(\"2026-06-22T00:00:00Z\");\n * const dt = dateTimeTool({}, { clock });\n * const { data } = await dt.invoke({ op: \"now\" });\n * console.log(data?.value); // \"2026-06-22T00:00:00.000Z\"\n */\nexport function dateTimeTool(\n options?: DateTimeOptions,\n deps?: DateTimeToolDeps,\n): ToolContract<DateTimeInput, DateTimeResult> {\n const clock: Clock = deps?.clock ?? Date.now;\n const defaultTimeZone = options?.defaultTimeZone;\n\n return tool<DateTimeInput, DateTimeResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n 'Work with dates and times. Set op to: \"now\" (current instant), ' +\n '\"add\" (shift iso by amount of unit), \"diff\" (signed to − from in unit; ' +\n \"pass the start instant as iso or from), \" +\n 'or \"format\" (render iso). Units are milliseconds, seconds, minutes, ' +\n \"hours, days, or weeks (month/year are not supported). Pass instants as \" +\n 'ISO-8601 strings; set format to \"iso\" (default), \"date\", \"time\", or ' +\n '\"datetime\", and timeZone as an IANA zone like \"Africa/Cairo\".',\n input: inputSchema,\n async execute(input) {\n const timeZone = input.timeZone ?? defaultTimeZone;\n\n switch (input.op) {\n case \"now\": {\n const date = new Date(clock());\n\n return { value: render(date, input.format, timeZone) };\n }\n\n case \"format\": {\n const date = parseIso(input.iso, \"iso\");\n\n return { value: render(date, input.format, timeZone) };\n }\n\n case \"add\": {\n const date = parseIso(input.iso, \"iso\");\n\n if (input.amount === undefined) {\n throw new DateTimeError('\"amount\" is required for the \"add\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n if (input.unit === undefined) {\n throw new DateTimeError('\"unit\" is required for the \"add\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n const shifted = new Date(date.getTime() + input.amount * unitToMs(input.unit));\n\n return { value: render(shifted, input.format, timeZone) };\n }\n\n case \"diff\": {\n // Accept `from` as an alias for `iso` — models naturally pass\n // `from` / `to` for a difference. `iso` wins when both are set.\n const from = parseIso(input.iso ?? input.from, \"iso (or from)\");\n const to = parseIso(input.to, \"to\");\n\n if (input.unit === undefined) {\n throw new DateTimeError('\"unit\" is required for the \"diff\" operation.', {\n type: \"invalid-input\",\n });\n }\n\n const diff = (to.getTime() - from.getTime()) / unitToMs(input.unit);\n\n return { value: String(diff) };\n }\n\n default: {\n // Exhaustiveness guard — the schema's literal union should make\n // this unreachable, but a future op added to the union without a\n // case here surfaces as typed data rather than silent fallthrough.\n const unreachable: never = input.op;\n\n throw new DateTimeError(`Unsupported operation \"${String(unreachable)}\".`, {\n type: \"unsupported-op\",\n });\n }\n }\n },\n });\n}\n"],"mappings":";;;;;;AAWA,MAAM,eAAe;;AAsBrB,MAAM,cAAc,aAA4B;CAC9C,IAAI,aAAyB;EApBK;EAAO;EAAO;EAAQ;CAoBzB,CAAC;CAChC,KAAK,oBAAoB;CACzB,MAAM,oBAAoB;CAC1B,IAAI,oBAAoB;CACxB,QAAQ,oBAAoB;CAC5B,MAAM,oBAAoB;CAC1B,UAAU,oBAAoB;CAC9B,QAAQ,oBAAoB;AAC9B,CAAC;;;;;;;AAQD,MAAM,UAAkC;CACtC,cAAc;CACd,SAAS;CACT,SAAS;CACT,OAAO;CACP,MAAM;CACN,OAAO;AACT;;AAGA,MAAM,eAAuC;CAC3C,aAAa;CACb,IAAI;CACJ,QAAQ;CACR,KAAK;CACL,GAAG;CACH,QAAQ;CACR,KAAK;CACL,GAAG;CACH,MAAM;CACN,IAAI;CACJ,GAAG;CACH,KAAK;CACL,GAAG;CACH,MAAM;CACN,GAAG;AACL;;AAGA,SAAS,SAAS,MAAsB;CACtC,MAAM,YAAY,aAAa,SAAS;CACxC,MAAM,KAAK,QAAQ;CAEnB,IAAI,OAAO,QACT,MAAM,IAAI,cACR,iBAAiB,KAAK,sBAAsB,OAAO,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,IAC5E,EAAE,MAAM,eAAe,CACzB;CAGF,OAAO;AACT;;;;;;;;AASA,SAAS,SAAS,KAAyB,OAAqB;CAC9D,IAAI,QAAQ,QACV,MAAM,IAAI,cAAc,IAAI,MAAM,oCAAoC,EACpE,MAAM,gBACR,CAAC;CAGH,MAAM,OAAO,IAAI,KAAK,GAAG;CAEzB,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAC7B,MAAM,IAAI,cAAc,IAAI,MAAM,sCAAsC,IAAI,KAAK,EAC/E,MAAM,gBACR,CAAC;CAGH,OAAO;AACT;;;;;;;;;;AAWA,SAAS,OAAO,MAAY,QAA4B,UAAsC;CAC5F,IAAI,WAAW,UAAa,WAAW,OAAO;EAG5C,IAAI,aAAa,QACf,eAAe,QAAQ;EAGzB,OAAO,KAAK,YAAY;CAC1B;CAEA,IAAI;EACF,OAAO,IAAI,KAAK,eAAe,SAAS;GACtC;GACA,WAAW,WAAW,SAAS,WAAW;GAC1C,WAAW,WAAW,SAAS,WAAW;GAC1C,GAAI,WAAW,aAAa;IAAE,WAAW;IAAU,WAAW;GAAS,IAAI,CAAC;EAC9E,CAAC,CAAC,CAAC,OAAO,IAAI;CAChB,SAAS,OAAO;EACd,MAAM,IAAI,cACR,iCAAiC,OAAO,GAAG,WAAW,kBAAkB,SAAS,KAAK,GAAG,IACzF;GAAE,MAAM;GAAqB,OAAO;EAAM,CAC5C;CACF;AACF;;;;;;;AAQA,SAAS,eAAe,UAAwB;CAC9C,IAAI;EACF,IAAI,KAAK,eAAe,SAAS,EAAE,SAAS,CAAC;CAC/C,SAAS,OAAO;EACd,MAAM,IAAI,cAAc,sBAAsB,SAAS,KAAK;GAC1D,MAAM;GACN,OAAO;EACT,CAAC;CACH;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,aACd,SACA,MAC6C;CAC7C,MAAM,QAAe,MAAM,SAAS,KAAK;CACzC,MAAM,kBAAkB,SAAS;CAEjC,OAAO,KAAoC;EACzC,MAAM,SAAS,QAAQ;EACvB,aACE;EAOF,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,MAAM,WAAW,MAAM,YAAY;GAEnC,QAAQ,MAAM,IAAd;IACE,KAAK,OAGH,OAAO,EAAE,OAAO,OAAO,IAFN,KAAK,MAAM,CAEF,GAAG,MAAM,QAAQ,QAAQ,EAAE;IAGvD,KAAK,UAGH,OAAO,EAAE,OAAO,OAFH,SAAS,MAAM,KAAK,KAEP,GAAG,MAAM,QAAQ,QAAQ,EAAE;IAGvD,KAAK,OAAO;KACV,MAAM,OAAO,SAAS,MAAM,KAAK,KAAK;KAEtC,IAAI,MAAM,WAAW,QACnB,MAAM,IAAI,cAAc,qDAAiD,EACvE,MAAM,gBACR,CAAC;KAGH,IAAI,MAAM,SAAS,QACjB,MAAM,IAAI,cAAc,mDAA+C,EACrE,MAAM,gBACR,CAAC;KAKH,OAAO,EAAE,OAAO,OAAO,IAFH,KAAK,KAAK,QAAQ,IAAI,MAAM,SAAS,SAAS,MAAM,IAAI,CAE/C,GAAG,MAAM,QAAQ,QAAQ,EAAE;IAC1D;IAEA,KAAK,QAAQ;KAGX,MAAM,OAAO,SAAS,MAAM,OAAO,MAAM,MAAM,eAAe;KAC9D,MAAM,KAAK,SAAS,MAAM,IAAI,IAAI;KAElC,IAAI,MAAM,SAAS,QACjB,MAAM,IAAI,cAAc,oDAAgD,EACtE,MAAM,gBACR,CAAC;KAGH,MAAM,QAAQ,GAAG,QAAQ,IAAI,KAAK,QAAQ,KAAK,SAAS,MAAM,IAAI;KAElE,OAAO,EAAE,OAAO,OAAO,IAAI,EAAE;IAC/B;IAEA,SAAS;KAIP,MAAM,cAAqB,MAAM;KAEjC,MAAM,IAAI,cAAc,0BAA0B,OAAO,WAAW,EAAE,KAAK,EACzE,MAAM,iBACR,CAAC;IACH;GACF;EACF;CACF,CAAC;AACH"}
@@ -1,2 +1,3 @@
1
1
  import { calculatorTool } from "./calculator.mjs";
2
- import { Clock, DateTimeToolDeps, dateTimeTool } from "./date-time.mjs";
2
+ import { Clock, DateTimeToolDeps, dateTimeTool } from "./date-time.mjs";
3
+ export { calculatorTool, dateTimeTool };
@@ -1,4 +1,4 @@
1
1
  import { calculatorTool } from "./calculator.mjs";
2
2
  import { dateTimeTool } from "./date-time.mjs";
3
3
 
4
- export { };
4
+ export { calculatorTool, dateTimeTool };
@@ -1 +1 @@
1
- {"version":3,"file":"schema.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/utility/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the utility tools' input validation. This package pins\n * only `@warlock.js/ai` as a runtime peer, so rather than pull in a\n * schema library we hand-roll the few field shapes the calculator and\n * date-time tools need — the same approach `@warlock.js/ai`'s own\n * `tool()` tests use. Each builder returns a {@link StandardSchemaV1},\n * which is what `tool({ input })` validates against before calling\n * `execute`.\n *\n * These intentionally cover only the primitive cases the UTILITY tools\n * require (`string`, `optional string`, `optional number`, and a\n * `literal union` enum). They are not a general-purpose validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-tools\";\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\ntype FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional string field — accepts `undefined` (the property absent or\n * explicitly undefined) or a string, and rejects every other type.\n */\nexport function optionalStringField(): FieldValidator<string | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"string\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a string when provided`, path: [key] }],\n };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [\n { message: `\"${key}\" must be a finite number when provided`, path: [key] },\n ],\n };\n };\n}\n\n/**\n * Required literal-union field — accepts only one of `allowed`, rejecting\n * everything else. Used for discriminators like the date-time `op`.\n *\n * @param allowed - The closed set of permitted string literals.\n */\nexport function literalField<T extends string>(\n allowed: readonly T[],\n): FieldValidator<T> {\n return (value, key) => {\n if (typeof value === \"string\" && (allowed as readonly string[]).includes(value)) {\n return { value: value as T };\n }\n\n return {\n issues: [\n {\n message: `\"${key}\" must be one of: ${allowed.join(\", \")}`,\n path: [key],\n },\n ],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ expression: string }>({\n * expression: stringField(),\n * });\n */\nexport function objectSchema<T extends object>(\n shape: ObjectShape<T>,\n): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,SAAS;;AAaf,SAAgB,cAAsC;CACpD,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAqB,MAAM,CAAC,GAAG;EAAE,CAAC,EAAE;CAC3E;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAmC,MAAM,CAAC,GAAG;EAAE,CAAC,EAC9E;CACF;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GACpD,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CACN;GAAE,SAAS,IAAI,IAAI;GAA0C,MAAM,CAAC,GAAG;EAAE,CAC3E,EACF;CACF;AACF;;;;;;;AAQA,SAAgB,aACd,SACmB;CACnB,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,YAAa,QAA8B,SAAS,KAAK,GAC5E,OAAO,EAAS,MAAW;EAG7B,OAAO,EACL,QAAQ,CACN;GACE,SAAS,IAAI,IAAI,oBAAoB,QAAQ,KAAK,IAAI;GACtD,MAAM,CAAC,GAAG;EACZ,CACF,EACF;CACF;AACF;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,aACd,OACqB;CACrB,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,SAAS,OAAO;GACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;GAG5D,MAAM,SAAS;GACf,MAAM,SAAmC,CAAC;GAC1C,MAAM,SAAkC,CAAC;GAEzC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAkB;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,UAAU,MAAM,OAAO,MAAgB,GAAa;IAE1D,IAAI,YAAY,SAAS;KACvB,OAAO,KAAK,GAAG,QAAQ,MAAM;KAE7B;IACF;IAKA,IAAI,QAAQ,UAAU,QACpB,OAAO,OAAiB,QAAQ;GAEpC;GAEA,IAAI,OAAO,SAAS,GAClB,OAAO,EAAE,OAAO;GAGlB,OAAO,EAAE,OAAO,OAAY;EAC9B;CACF,EACF;AACF"}
1
+ {"version":3,"file":"schema.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/utility/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the utility tools' input validation. This package pins\n * only `@warlock.js/ai` as a runtime peer, so rather than pull in a\n * schema library we hand-roll the few field shapes the calculator and\n * date-time tools need — the same approach `@warlock.js/ai`'s own\n * `tool()` tests use. Each builder returns a {@link StandardSchemaV1},\n * which is what `tool({ input })` validates against before calling\n * `execute`.\n *\n * These intentionally cover only the primitive cases the UTILITY tools\n * require (`string`, `optional string`, `optional number`, and a\n * `literal union` enum). They are not a general-purpose validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-tools\";\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\ntype FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional string field — accepts `undefined` (the property absent or\n * explicitly undefined) or a string, and rejects every other type.\n */\nexport function optionalStringField(): FieldValidator<string | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"string\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a string when provided`, path: [key] }],\n };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a finite number when provided`, path: [key] }],\n };\n };\n}\n\n/**\n * Required literal-union field — accepts only one of `allowed`, rejecting\n * everything else. Used for discriminators like the date-time `op`.\n *\n * @param allowed - The closed set of permitted string literals.\n */\nexport function literalField<T extends string>(allowed: readonly T[]): FieldValidator<T> {\n return (value, key) => {\n if (typeof value === \"string\" && (allowed as readonly string[]).includes(value)) {\n return { value: value as T };\n }\n\n return {\n issues: [\n {\n message: `\"${key}\" must be one of: ${allowed.join(\", \")}`,\n path: [key],\n },\n ],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ expression: string }>({\n * expression: stringField(),\n * });\n */\nexport function objectSchema<T extends object>(shape: ObjectShape<T>): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,SAAS;;AAaf,SAAgB,cAAsC;CACpD,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAqB,MAAM,CAAC,GAAG;EAAE,CAAC,EAAE;CAC3E;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAmC,MAAM,CAAC,GAAG;EAAE,CAAC,EAC9E;CACF;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GACpD,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAA0C,MAAM,CAAC,GAAG;EAAE,CAAC,EACrF;CACF;AACF;;;;;;;AAQA,SAAgB,aAA+B,SAA0C;CACvF,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,YAAa,QAA8B,SAAS,KAAK,GAC5E,OAAO,EAAS,MAAW;EAG7B,OAAO,EACL,QAAQ,CACN;GACE,SAAS,IAAI,IAAI,oBAAoB,QAAQ,KAAK,IAAI;GACtD,MAAM,CAAC,GAAG;EACZ,CACF,EACF;CACF;AACF;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,aAA+B,OAA4C;CACzF,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,SAAS,OAAO;GACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;GAG5D,MAAM,SAAS;GACf,MAAM,SAAmC,CAAC;GAC1C,MAAM,SAAkC,CAAC;GAEzC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAkB;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,UAAU,MAAM,OAAO,MAAgB,GAAa;IAE1D,IAAI,YAAY,SAAS;KACvB,OAAO,KAAK,GAAG,QAAQ,MAAM;KAE7B;IACF;IAKA,IAAI,QAAQ,UAAU,QACpB,OAAO,OAAiB,QAAQ;GAEpC;GAEA,IAAI,OAAO,SAAS,GAClB,OAAO,EAAE,OAAO;GAGlB,OAAO,EAAE,OAAO,OAAY;EAC9B;CACF,EACF;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { FetchUrlInput, FetchUrlOptions, FetchUrlResult } from "../contracts/web.type.mjs";
2
+ import "../contracts/index.mjs";
2
3
  import { ToolContract } from "@warlock.js/ai";
3
-
4
4
  //#region ../../packages/ai-tools/src/web/fetch-url.d.ts
5
5
  /**
6
6
  * Build the agent-facing `fetch_url` tool — fetch a URL over the global
@@ -41,7 +41,6 @@ import { ToolContract } from "@warlock.js/ai";
41
41
  * const { data } = await fetchUrl.invoke({ url: "https://docs.stripe.com/api" });
42
42
  * console.log(data?.content, data?.truncated);
43
43
  */
44
- declare function fetchUrlTool(options?: FetchUrlOptions): ToolContract<FetchUrlInput, FetchUrlResult>;
44
+ export declare function fetchUrlTool(options?: FetchUrlOptions): ToolContract<FetchUrlInput, FetchUrlResult>;
45
45
  //#endregion
46
- export { fetchUrlTool };
47
46
  //# sourceMappingURL=fetch-url.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-url.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/fetch-url.ts"],"mappings":";;;;;;;AA0TA;;;;;;;;;;;;;;;AAE6C;;;;;;;;;;;;;;;;;;;;;iBAF7B,YAAA,CACd,OAAA,GAAU,eAAA,GACT,YAAA,CAAa,aAAA,EAAe,cAAA"}
1
+ {"version":3,"file":"fetch-url.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/fetch-url.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA2SgB,aACd,UAAU,kBACT,aAAa,eAAe"}
@@ -224,7 +224,8 @@ function fetchUrlTool(options) {
224
224
  type: "denied-host",
225
225
  cause
226
226
  });
227
- throw new WebToolError(`fetch_url request failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
227
+ const message = cause instanceof Error ? cause.message : String(cause);
228
+ throw new WebToolError(`fetch_url request failed: ${message}`, {
228
229
  type: "request-failed",
229
230
  cause
230
231
  });
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-url.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/fetch-url.ts"],"sourcesContent":["import {\n guardedFetch,\n OutboundPolicyError,\n tool,\n type ToolContract,\n} from \"@warlock.js/ai\";\nimport type {\n FetchUrlExtract,\n FetchUrlInput,\n FetchUrlOptions,\n FetchUrlResult,\n} from \"../contracts\";\nimport { WebToolError } from \"../errors\";\nimport { objectSchema, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"fetch_url\";\n\n/** Default hard cap on response bytes read before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** Default per-request timeout in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default rendering mode for the fetched document. */\nconst DEFAULT_EXTRACT: FetchUrlExtract = \"text\";\n\n/** Input schema: `{ url }`. */\nconst inputSchema = objectSchema<FetchUrlInput>({\n url: stringField(),\n});\n\n// ============================================================\n// Lazily-loaded @mozilla/readability + jsdom (OPTIONAL peers)\n// ============================================================\n\n/**\n * Minimal structural shapes of the optional `@mozilla/readability` + `jsdom`\n * peers — only the members this file actually uses. Declared locally so the\n * package type-checks even when the peers are NOT installed (they are lazy\n * optional peers, loaded below via a `string` specifier so `tsc` never tries\n * to statically resolve them).\n */\ninterface ReadabilityArticleLike {\n readonly content?: string | null;\n readonly textContent?: string | null;\n}\ninterface ReadabilityInstanceLike {\n parse(): ReadabilityArticleLike | null;\n}\ninterface ReadabilityModuleLike {\n Readability: new (document: unknown) => ReadabilityInstanceLike;\n}\ninterface JsdomInstanceLike {\n readonly window: { readonly document: unknown };\n}\ninterface JsdomModuleLike {\n JSDOM: new (html: string, options?: { url?: string }) => JsdomInstanceLike;\n}\n\n// String specifiers typed `string` (not a literal) so `tsc` does not attempt\n// to resolve these optional peers at build time.\nconst READABILITY_MODULE_ID: string = \"@mozilla/readability\";\nconst JSDOM_MODULE_ID: string = \"jsdom\";\n\nlet ReadabilitySdk: ReadabilityModuleLike;\nlet JsdomSdk: JsdomModuleLike;\nlet isReadabilityAvailable: boolean | undefined;\nlet loadingPromise: Promise<void> | undefined;\n\nconst READABILITY_INSTALL_INSTRUCTIONS = `\nThe fetch_url text/markdown extractor requires the @mozilla/readability and jsdom packages.\nInstall them with:\n\n npm install @mozilla/readability jsdom\n\nOr with your preferred package manager:\n\n pnpm add @mozilla/readability jsdom\n yarn add @mozilla/readability jsdom\n`.trim();\n\n/**\n * Settle the lazy import of `@mozilla/readability` + `jsdom` once,\n * concurrency-safe. Only needed for the `\"text\"` / `\"markdown\"` extract\n * modes — `\"html\"` returns the raw body and never loads them. A bare\n * `catch` flips the flag to `false`; the curated install string surfaces\n * at use time via {@link WebToolError}, never a raw module-resolution\n * stack trace.\n */\nfunction loadReadability(): Promise<void> {\n if (isReadabilityAvailable !== undefined) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n ReadabilitySdk = (await import(READABILITY_MODULE_ID)) as ReadabilityModuleLike;\n JsdomSdk = (await import(JSDOM_MODULE_ID)) as JsdomModuleLike;\n isReadabilityAvailable = true;\n } catch {\n isReadabilityAvailable = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * Parse the input URL, rejecting anything unparseable or not over\n * `http`/`https` with a typed {@link WebToolError}.\n */\nfunction parseUrl(raw: string): URL {\n let url: URL;\n\n try {\n url = new URL(raw);\n } catch {\n throw new WebToolError(`fetch_url received an unparseable URL: \"${raw}\".`, {\n type: \"invalid-url\",\n });\n }\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new WebToolError(\n `fetch_url only supports http(s) URLs; got \"${url.protocol}\".`,\n { type: \"invalid-url\" },\n );\n }\n\n return url;\n}\n\n/**\n * Enforce the host allowlist (an SSRF guardrail) before any network call.\n * When `allowHosts` is set, a request to a host not in it is rejected;\n * matching is exact on the URL hostname (case-insensitive).\n */\nfunction assertHostAllowed(url: URL, allowHosts?: string[]): void {\n if (!allowHosts || allowHosts.length === 0) {\n return;\n }\n\n const host = url.hostname.toLowerCase();\n const allowed = allowHosts.some((entry) => entry.toLowerCase() === host);\n\n if (!allowed) {\n throw new WebToolError(\n `fetch_url blocked host \"${url.hostname}\" — it is not in the allowHosts allowlist.`,\n { type: \"denied-host\", context: { host: url.hostname } },\n );\n }\n}\n\n/**\n * Read the response body up to `maxBytes`, stopping early once the cap is\n * reached. Returns the decoded text and whether it was truncated. When\n * the body has no stream (a stubbed `Response`), falls back to `.text()`\n * and truncates the decoded string at `maxBytes`.\n */\nasync function readBody(\n response: Response,\n maxBytes: number,\n): Promise<{ body: string; truncated: boolean }> {\n const stream = response.body;\n\n if (!stream) {\n const text = await response.text();\n\n if (text.length > maxBytes) {\n return { body: text.slice(0, maxBytes), truncated: true };\n }\n\n return { body: text, truncated: false };\n }\n\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let received = 0;\n let truncated = false;\n\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (value) {\n received += value.byteLength;\n chunks.push(value);\n\n if (received >= maxBytes) {\n truncated = true;\n await reader.cancel();\n break;\n }\n }\n }\n\n const merged = new Uint8Array(received);\n let offset = 0;\n\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const sliced = truncated ? merged.subarray(0, maxBytes) : merged;\n const body = new TextDecoder().decode(sliced);\n\n return { body, truncated };\n}\n\n/**\n * Render the fetched HTML into the configured `extract` mode. `\"html\"`\n * returns the raw body untouched; `\"text\"` and `\"markdown\"` run\n * `@mozilla/readability` over a `jsdom` document, throwing a curated\n * {@link WebToolError} when those optional peers are absent. `\"markdown\"`\n * applies a light heading/paragraph conversion over the extracted text.\n */\nasync function render(\n html: string,\n url: string,\n extract: FetchUrlExtract,\n): Promise<string> {\n if (extract === \"html\") {\n return html;\n }\n\n await loadReadability();\n\n if (!isReadabilityAvailable) {\n throw new WebToolError(READABILITY_INSTALL_INSTRUCTIONS, { type: \"missing-peer\" });\n }\n\n const dom = new JsdomSdk.JSDOM(html, { url });\n const article = new ReadabilitySdk.Readability(dom.window.document).parse();\n\n if (extract === \"markdown\") {\n return article?.content ? htmlToMarkdown(article.content) : (article?.textContent ?? \"\");\n }\n\n return article?.textContent ?? \"\";\n}\n\n/**\n * Minimal HTML→Markdown reduction for readability's extracted article\n * HTML — headings become `#` prefixes, paragraphs/line-breaks become\n * blank-line separated blocks, and remaining tags are stripped. This is a\n * pragmatic conversion, not a full CommonMark serializer.\n */\nfunction htmlToMarkdown(html: string): string {\n return html\n .replace(/<h([1-6])[^>]*>(.*?)<\\/h\\1>/gis, (_match, level: string, text: string) => {\n const hashes = \"#\".repeat(Number(level));\n\n return `\\n\\n${hashes} ${stripTags(text).trim()}\\n\\n`;\n })\n .replace(/<\\/(p|div|section|article|li)>/gi, \"\\n\\n\")\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\n/** Strip any remaining HTML tags from a fragment. */\nfunction stripTags(html: string): string {\n return html.replace(/<[^>]+>/g, \"\");\n}\n\n/**\n * Build the agent-facing `fetch_url` tool — fetch a URL over the global\n * `fetch` (Node 18+) and hand the model back rendered `content`.\n *\n * Guardrails, applied in order before/around the network call:\n * - **Private-network deny (default).** Every request — and every\n * redirect hop — goes through the framework's `guardedFetch` outbound\n * policy, which refuses private / loopback / link-local /\n * cloud-metadata addresses (and hostnames resolving to them) unless\n * `allowPrivateNetwork: true` is set. This applies even when\n * `allowHosts` is not configured, so a bare `ai.tools.fetchUrl()` is\n * not an SSRF primitive.\n * - **`allowHosts`** — when set, a request to any host not in the list is\n * rejected before the fetch, and redirect targets are held to the same\n * allowlist.\n * - **`timeoutMs`** — the request is aborted via `AbortSignal.timeout`\n * (default {@link DEFAULT_TIMEOUT_MS}).\n * - **`maxBytes`** — the response body is read up to this cap and the\n * result flags `truncated` when it was cut off (default\n * {@link DEFAULT_MAX_BYTES}).\n * - **`extract`** — `\"html\"` returns the raw body; `\"text\"` (default) and\n * `\"markdown\"` run `@mozilla/readability` over `jsdom`, lazily imported\n * so they stay OPTIONAL peers — a missing peer throws the curated\n * install string.\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * throws a typed {@link WebToolError}; the `tool()` wrapper catches it and\n * surfaces it in the returned `{ error }` field — `invoke()` never throws\n * — so the agent reads the failure and self-corrects.\n *\n * @param options - Tool-name override, byte cap, timeout, extract mode,\n * and host allowlist.\n * @returns A `ToolContract<{ url }, FetchUrlResult>`.\n *\n * @example\n * const fetchUrl = fetchUrlTool({ extract: \"text\", allowHosts: [\"docs.stripe.com\"] });\n * const { data } = await fetchUrl.invoke({ url: \"https://docs.stripe.com/api\" });\n * console.log(data?.content, data?.truncated);\n */\nexport function fetchUrlTool(\n options?: FetchUrlOptions,\n): ToolContract<FetchUrlInput, FetchUrlResult> {\n const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;\n const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const extract = options?.extract ?? DEFAULT_EXTRACT;\n const allowHosts = options?.allowHosts;\n const allowPrivateNetwork = options?.allowPrivateNetwork ?? false;\n\n // Warm the readability peers non-blockingly when the configured mode\n // needs them, so the curated install string is ready (and logged at\n // first use) without delaying construction.\n if (extract !== \"html\") {\n void loadReadability();\n }\n\n return tool<FetchUrlInput, FetchUrlResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Fetch a web page or document by URL and return its main content as \" +\n \"text (readability-extracted), raw HTML, or Markdown. Use to read a \" +\n \"specific page you already have the URL for.\",\n input: inputSchema,\n async execute(input) {\n const url = parseUrl(input.url);\n assertHostAllowed(url, allowHosts);\n\n let response: Response;\n\n try {\n // The shared hardened outbound path: private-IP deny by default,\n // per-hop redirect re-validation, timeout — never a raw fetch.\n response = await guardedFetch(url.toString(), {\n allowedSchemes: [\"http\", \"https\"],\n hostAllowlist: allowHosts,\n denyPrivateIPsAfterDNS: !allowPrivateNetwork,\n timeoutMs,\n });\n } catch (cause) {\n if (cause instanceof OutboundPolicyError) {\n throw new WebToolError(`fetch_url blocked: ${cause.message}`, {\n type: \"denied-host\",\n cause,\n });\n }\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new WebToolError(`fetch_url request failed: ${message}`, {\n type: \"request-failed\",\n cause,\n });\n }\n\n const { body, truncated } = await readBody(response, maxBytes);\n const content = await render(body, response.url || url.toString(), extract);\n\n return {\n url: response.url || url.toString(),\n status: response.status,\n content,\n truncated,\n };\n },\n });\n}\n"],"mappings":";;;;;;AAgBA,MAAM,eAAe;;AAGrB,MAAM,oBAAoB;;AAG1B,MAAM,qBAAqB;;AAG3B,MAAM,kBAAmC;;AAGzC,MAAM,cAAc,aAA4B,EAC9C,KAAK,YAAY,EACnB,CAAC;AAgCD,MAAM,wBAAgC;AACtC,MAAM,kBAA0B;AAEhC,IAAI;AACJ,IAAI;AACJ,IAAI;AACJ,IAAI;AAEJ,MAAM,mCAAmC;;;;;;;;;;EAUvC,KAAK;;;;;;;;;AAUP,SAAS,kBAAiC;CACxC,IAAI,2BAA2B,QAC7B,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,iBAAkB,MAAM,OAAO;GAC/B,WAAY,MAAM,OAAO;GACzB,yBAAyB;EAC3B,QAAQ;GACN,yBAAyB;EAC3B;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;AAMA,SAAS,SAAS,KAAkB;CAClC,IAAI;CAEJ,IAAI;EACF,MAAM,IAAI,IAAI,GAAG;CACnB,QAAQ;EACN,MAAM,IAAI,aAAa,2CAA2C,IAAI,KAAK,EACzE,MAAM,cACR,CAAC;CACH;CAEA,IAAI,IAAI,aAAa,WAAW,IAAI,aAAa,UAC/C,MAAM,IAAI,aACR,8CAA8C,IAAI,SAAS,KAC3D,EAAE,MAAM,cAAc,CACxB;CAGF,OAAO;AACT;;;;;;AAOA,SAAS,kBAAkB,KAAU,YAA6B;CAChE,IAAI,CAAC,cAAc,WAAW,WAAW,GACvC;CAGF,MAAM,OAAO,IAAI,SAAS,YAAY;CAGtC,IAAI,CAFY,WAAW,MAAM,UAAU,MAAM,YAAY,MAAM,IAExD,GACT,MAAM,IAAI,aACR,2BAA2B,IAAI,SAAS,6CACxC;EAAE,MAAM;EAAe,SAAS,EAAE,MAAM,IAAI,SAAS;CAAE,CACzD;AAEJ;;;;;;;AAQA,eAAe,SACb,UACA,UAC+C;CAC/C,MAAM,SAAS,SAAS;CAExB,IAAI,CAAC,QAAQ;EACX,MAAM,OAAO,MAAM,SAAS,KAAK;EAEjC,IAAI,KAAK,SAAS,UAChB,OAAO;GAAE,MAAM,KAAK,MAAM,GAAG,QAAQ;GAAG,WAAW;EAAK;EAG1D,OAAO;GAAE,MAAM;GAAM,WAAW;EAAM;CACxC;CAEA,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,SAAuB,CAAC;CAC9B,IAAI,WAAW;CACf,IAAI,YAAY;CAEhB,SAAS;EACP,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;EAE1C,IAAI,MACF;EAGF,IAAI,OAAO;GACT,YAAY,MAAM;GAClB,OAAO,KAAK,KAAK;GAEjB,IAAI,YAAY,UAAU;IACxB,YAAY;IACZ,MAAM,OAAO,OAAO;IACpB;GACF;EACF;CACF;CAEA,MAAM,SAAS,IAAI,WAAW,QAAQ;CACtC,IAAI,SAAS;CAEb,KAAK,MAAM,SAAS,QAAQ;EAC1B,OAAO,IAAI,OAAO,MAAM;EACxB,UAAU,MAAM;CAClB;CAEA,MAAM,SAAS,YAAY,OAAO,SAAS,GAAG,QAAQ,IAAI;CAG1D,OAAO;EAAE,MAFI,IAAI,YAAY,CAAC,CAAC,OAAO,MAE1B;EAAG;CAAU;AAC3B;;;;;;;;AASA,eAAe,OACb,MACA,KACA,SACiB;CACjB,IAAI,YAAY,QACd,OAAO;CAGT,MAAM,gBAAgB;CAEtB,IAAI,CAAC,wBACH,MAAM,IAAI,aAAa,kCAAkC,EAAE,MAAM,eAAe,CAAC;CAGnF,MAAM,MAAM,IAAI,SAAS,MAAM,MAAM,EAAE,IAAI,CAAC;CAC5C,MAAM,UAAU,IAAI,eAAe,YAAY,IAAI,OAAO,QAAQ,CAAC,CAAC,MAAM;CAE1E,IAAI,YAAY,YACd,OAAO,SAAS,UAAU,eAAe,QAAQ,OAAO,IAAK,SAAS,eAAe;CAGvF,OAAO,SAAS,eAAe;AACjC;;;;;;;AAQA,SAAS,eAAe,MAAsB;CAC5C,OAAO,KACJ,QAAQ,mCAAmC,QAAQ,OAAe,SAAiB;EAGlF,OAAO,OAFQ,IAAI,OAAO,OAAO,KAAK,CAEnB,EAAE,GAAG,UAAU,IAAI,CAAC,CAAC,KAAK,EAAE;CACjD,CAAC,CAAC,CACD,QAAQ,oCAAoC,MAAM,CAAC,CACnD,QAAQ,gBAAgB,IAAI,CAAC,CAC7B,QAAQ,YAAY,EAAE,CAAC,CACvB,QAAQ,WAAW,MAAM,CAAC,CAC1B,KAAK;AACV;;AAGA,SAAS,UAAU,MAAsB;CACvC,OAAO,KAAK,QAAQ,YAAY,EAAE;AACpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,aACd,SAC6C;CAC7C,MAAM,WAAW,SAAS,YAAY;CACtC,MAAM,YAAY,SAAS,aAAa;CACxC,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,aAAa,SAAS;CAC5B,MAAM,sBAAsB,SAAS,uBAAuB;CAK5D,IAAI,YAAY,QACd,AAAK,gBAAgB;CAGvB,OAAO,KAAoC;EACzC,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,MAAM,MAAM,SAAS,MAAM,GAAG;GAC9B,kBAAkB,KAAK,UAAU;GAEjC,IAAI;GAEJ,IAAI;IAGF,WAAW,MAAM,aAAa,IAAI,SAAS,GAAG;KAC5C,gBAAgB,CAAC,QAAQ,OAAO;KAChC,eAAe;KACf,wBAAwB,CAAC;KACzB;IACF,CAAC;GACH,SAAS,OAAO;IACd,IAAI,iBAAiB,qBACnB,MAAM,IAAI,aAAa,sBAAsB,MAAM,WAAW;KAC5D,MAAM;KACN;IACF,CAAC;IAKH,MAAM,IAAI,aAAa,6BAFP,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAEN;KAC7D,MAAM;KACN;IACF,CAAC;GACH;GAEA,MAAM,EAAE,MAAM,cAAc,MAAM,SAAS,UAAU,QAAQ;GAC7D,MAAM,UAAU,MAAM,OAAO,MAAM,SAAS,OAAO,IAAI,SAAS,GAAG,OAAO;GAE1E,OAAO;IACL,KAAK,SAAS,OAAO,IAAI,SAAS;IAClC,QAAQ,SAAS;IACjB;IACA;GACF;EACF;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"fetch-url.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/fetch-url.ts"],"sourcesContent":["import { guardedFetch, OutboundPolicyError, tool, type ToolContract } from \"@warlock.js/ai\";\nimport type { FetchUrlExtract, FetchUrlInput, FetchUrlOptions, FetchUrlResult } from \"../contracts\";\nimport { WebToolError } from \"../errors\";\nimport { objectSchema, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"fetch_url\";\n\n/** Default hard cap on response bytes read before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** Default per-request timeout in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default rendering mode for the fetched document. */\nconst DEFAULT_EXTRACT: FetchUrlExtract = \"text\";\n\n/** Input schema: `{ url }`. */\nconst inputSchema = objectSchema<FetchUrlInput>({\n url: stringField(),\n});\n\n// ============================================================\n// Lazily-loaded @mozilla/readability + jsdom (OPTIONAL peers)\n// ============================================================\n\n/**\n * Minimal structural shapes of the optional `@mozilla/readability` + `jsdom`\n * peers — only the members this file actually uses. Declared locally so the\n * package type-checks even when the peers are NOT installed (they are lazy\n * optional peers, loaded below via a `string` specifier so `tsc` never tries\n * to statically resolve them).\n */\ninterface ReadabilityArticleLike {\n readonly content?: string | null;\n readonly textContent?: string | null;\n}\ninterface ReadabilityInstanceLike {\n parse(): ReadabilityArticleLike | null;\n}\ninterface ReadabilityModuleLike {\n Readability: new (document: unknown) => ReadabilityInstanceLike;\n}\ninterface JsdomInstanceLike {\n readonly window: { readonly document: unknown };\n}\ninterface JsdomModuleLike {\n JSDOM: new (html: string, options?: { url?: string }) => JsdomInstanceLike;\n}\n\n// String specifiers typed `string` (not a literal) so `tsc` does not attempt\n// to resolve these optional peers at build time.\nconst READABILITY_MODULE_ID: string = \"@mozilla/readability\";\nconst JSDOM_MODULE_ID: string = \"jsdom\";\n\nlet ReadabilitySdk: ReadabilityModuleLike;\nlet JsdomSdk: JsdomModuleLike;\nlet isReadabilityAvailable: boolean | undefined;\nlet loadingPromise: Promise<void> | undefined;\n\nconst READABILITY_INSTALL_INSTRUCTIONS = `\nThe fetch_url text/markdown extractor requires the @mozilla/readability and jsdom packages.\nInstall them with:\n\n npm install @mozilla/readability jsdom\n\nOr with your preferred package manager:\n\n pnpm add @mozilla/readability jsdom\n yarn add @mozilla/readability jsdom\n`.trim();\n\n/**\n * Settle the lazy import of `@mozilla/readability` + `jsdom` once,\n * concurrency-safe. Only needed for the `\"text\"` / `\"markdown\"` extract\n * modes — `\"html\"` returns the raw body and never loads them. A bare\n * `catch` flips the flag to `false`; the curated install string surfaces\n * at use time via {@link WebToolError}, never a raw module-resolution\n * stack trace.\n */\nfunction loadReadability(): Promise<void> {\n if (isReadabilityAvailable !== undefined) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n ReadabilitySdk = (await import(READABILITY_MODULE_ID)) as ReadabilityModuleLike;\n JsdomSdk = (await import(JSDOM_MODULE_ID)) as JsdomModuleLike;\n isReadabilityAvailable = true;\n } catch {\n isReadabilityAvailable = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * Parse the input URL, rejecting anything unparseable or not over\n * `http`/`https` with a typed {@link WebToolError}.\n */\nfunction parseUrl(raw: string): URL {\n let url: URL;\n\n try {\n url = new URL(raw);\n } catch {\n throw new WebToolError(`fetch_url received an unparseable URL: \"${raw}\".`, {\n type: \"invalid-url\",\n });\n }\n\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new WebToolError(`fetch_url only supports http(s) URLs; got \"${url.protocol}\".`, {\n type: \"invalid-url\",\n });\n }\n\n return url;\n}\n\n/**\n * Enforce the host allowlist (an SSRF guardrail) before any network call.\n * When `allowHosts` is set, a request to a host not in it is rejected;\n * matching is exact on the URL hostname (case-insensitive).\n */\nfunction assertHostAllowed(url: URL, allowHosts?: string[]): void {\n if (!allowHosts || allowHosts.length === 0) {\n return;\n }\n\n const host = url.hostname.toLowerCase();\n const allowed = allowHosts.some((entry) => entry.toLowerCase() === host);\n\n if (!allowed) {\n throw new WebToolError(\n `fetch_url blocked host \"${url.hostname}\" — it is not in the allowHosts allowlist.`,\n { type: \"denied-host\", context: { host: url.hostname } },\n );\n }\n}\n\n/**\n * Read the response body up to `maxBytes`, stopping early once the cap is\n * reached. Returns the decoded text and whether it was truncated. When\n * the body has no stream (a stubbed `Response`), falls back to `.text()`\n * and truncates the decoded string at `maxBytes`.\n */\nasync function readBody(\n response: Response,\n maxBytes: number,\n): Promise<{ body: string; truncated: boolean }> {\n const stream = response.body;\n\n if (!stream) {\n const text = await response.text();\n\n if (text.length > maxBytes) {\n return { body: text.slice(0, maxBytes), truncated: true };\n }\n\n return { body: text, truncated: false };\n }\n\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let received = 0;\n let truncated = false;\n\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (value) {\n received += value.byteLength;\n chunks.push(value);\n\n if (received >= maxBytes) {\n truncated = true;\n await reader.cancel();\n break;\n }\n }\n }\n\n const merged = new Uint8Array(received);\n let offset = 0;\n\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const sliced = truncated ? merged.subarray(0, maxBytes) : merged;\n const body = new TextDecoder().decode(sliced);\n\n return { body, truncated };\n}\n\n/**\n * Render the fetched HTML into the configured `extract` mode. `\"html\"`\n * returns the raw body untouched; `\"text\"` and `\"markdown\"` run\n * `@mozilla/readability` over a `jsdom` document, throwing a curated\n * {@link WebToolError} when those optional peers are absent. `\"markdown\"`\n * applies a light heading/paragraph conversion over the extracted text.\n */\nasync function render(html: string, url: string, extract: FetchUrlExtract): Promise<string> {\n if (extract === \"html\") {\n return html;\n }\n\n await loadReadability();\n\n if (!isReadabilityAvailable) {\n throw new WebToolError(READABILITY_INSTALL_INSTRUCTIONS, { type: \"missing-peer\" });\n }\n\n const dom = new JsdomSdk.JSDOM(html, { url });\n const article = new ReadabilitySdk.Readability(dom.window.document).parse();\n\n if (extract === \"markdown\") {\n return article?.content ? htmlToMarkdown(article.content) : (article?.textContent ?? \"\");\n }\n\n return article?.textContent ?? \"\";\n}\n\n/**\n * Minimal HTML→Markdown reduction for readability's extracted article\n * HTML — headings become `#` prefixes, paragraphs/line-breaks become\n * blank-line separated blocks, and remaining tags are stripped. This is a\n * pragmatic conversion, not a full CommonMark serializer.\n */\nfunction htmlToMarkdown(html: string): string {\n return html\n .replace(/<h([1-6])[^>]*>(.*?)<\\/h\\1>/gis, (_match, level: string, text: string) => {\n const hashes = \"#\".repeat(Number(level));\n\n return `\\n\\n${hashes} ${stripTags(text).trim()}\\n\\n`;\n })\n .replace(/<\\/(p|div|section|article|li)>/gi, \"\\n\\n\")\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\n/** Strip any remaining HTML tags from a fragment. */\nfunction stripTags(html: string): string {\n return html.replace(/<[^>]+>/g, \"\");\n}\n\n/**\n * Build the agent-facing `fetch_url` tool — fetch a URL over the global\n * `fetch` (Node 18+) and hand the model back rendered `content`.\n *\n * Guardrails, applied in order before/around the network call:\n * - **Private-network deny (default).** Every request — and every\n * redirect hop — goes through the framework's `guardedFetch` outbound\n * policy, which refuses private / loopback / link-local /\n * cloud-metadata addresses (and hostnames resolving to them) unless\n * `allowPrivateNetwork: true` is set. This applies even when\n * `allowHosts` is not configured, so a bare `ai.tools.fetchUrl()` is\n * not an SSRF primitive.\n * - **`allowHosts`** — when set, a request to any host not in the list is\n * rejected before the fetch, and redirect targets are held to the same\n * allowlist.\n * - **`timeoutMs`** — the request is aborted via `AbortSignal.timeout`\n * (default {@link DEFAULT_TIMEOUT_MS}).\n * - **`maxBytes`** — the response body is read up to this cap and the\n * result flags `truncated` when it was cut off (default\n * {@link DEFAULT_MAX_BYTES}).\n * - **`extract`** — `\"html\"` returns the raw body; `\"text\"` (default) and\n * `\"markdown\"` run `@mozilla/readability` over `jsdom`, lazily imported\n * so they stay OPTIONAL peers — a missing peer throws the curated\n * install string.\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * throws a typed {@link WebToolError}; the `tool()` wrapper catches it and\n * surfaces it in the returned `{ error }` field — `invoke()` never throws\n * — so the agent reads the failure and self-corrects.\n *\n * @param options - Tool-name override, byte cap, timeout, extract mode,\n * and host allowlist.\n * @returns A `ToolContract<{ url }, FetchUrlResult>`.\n *\n * @example\n * const fetchUrl = fetchUrlTool({ extract: \"text\", allowHosts: [\"docs.stripe.com\"] });\n * const { data } = await fetchUrl.invoke({ url: \"https://docs.stripe.com/api\" });\n * console.log(data?.content, data?.truncated);\n */\nexport function fetchUrlTool(\n options?: FetchUrlOptions,\n): ToolContract<FetchUrlInput, FetchUrlResult> {\n const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;\n const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const extract = options?.extract ?? DEFAULT_EXTRACT;\n const allowHosts = options?.allowHosts;\n const allowPrivateNetwork = options?.allowPrivateNetwork ?? false;\n\n // Warm the readability peers non-blockingly when the configured mode\n // needs them, so the curated install string is ready (and logged at\n // first use) without delaying construction.\n if (extract !== \"html\") {\n void loadReadability();\n }\n\n return tool<FetchUrlInput, FetchUrlResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Fetch a web page or document by URL and return its main content as \" +\n \"text (readability-extracted), raw HTML, or Markdown. Use to read a \" +\n \"specific page you already have the URL for.\",\n input: inputSchema,\n async execute(input) {\n const url = parseUrl(input.url);\n assertHostAllowed(url, allowHosts);\n\n let response: Response;\n\n try {\n // The shared hardened outbound path: private-IP deny by default,\n // per-hop redirect re-validation, timeout — never a raw fetch.\n response = await guardedFetch(url.toString(), {\n allowedSchemes: [\"http\", \"https\"],\n hostAllowlist: allowHosts,\n denyPrivateIPsAfterDNS: !allowPrivateNetwork,\n timeoutMs,\n });\n } catch (cause) {\n if (cause instanceof OutboundPolicyError) {\n throw new WebToolError(`fetch_url blocked: ${cause.message}`, {\n type: \"denied-host\",\n cause,\n });\n }\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new WebToolError(`fetch_url request failed: ${message}`, {\n type: \"request-failed\",\n cause,\n });\n }\n\n const { body, truncated } = await readBody(response, maxBytes);\n const content = await render(body, response.url || url.toString(), extract);\n\n return {\n url: response.url || url.toString(),\n status: response.status,\n content,\n truncated,\n };\n },\n });\n}\n"],"mappings":"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"}
@@ -1,2 +1,3 @@
1
1
  import { webSearchTool } from "./web-search.mjs";
2
- import { fetchUrlTool } from "./fetch-url.mjs";
2
+ import { fetchUrlTool } from "./fetch-url.mjs";
3
+ export { fetchUrlTool, webSearchTool };
package/esm/web/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  import { fetchUrlTool } from "./fetch-url.mjs";
2
2
  import { webSearchTool } from "./web-search.mjs";
3
3
 
4
- export { };
4
+ export { fetchUrlTool, webSearchTool };
@@ -1 +1 @@
1
- {"version":3,"file":"schema.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the web tools' input validation. The package pins only\n * `@warlock.js/ai` as a runtime peer, so rather than pull in a schema\n * library we hand-roll the few shapes the web tools need — exactly the\n * pattern `@warlock.js/ai`'s own `tool()` tests use and the sibling\n * `@warlock.js/ai-workspace` package follows. Each builder returns a\n * `StandardSchemaV1`, which is what `tool({ input })` validates against\n * before calling `execute`.\n *\n * These intentionally cover only the primitive cases the WEB tools\n * require (`string`, `optional number`, and an `object` of fields). They\n * are not a general-purpose validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-tools\";\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\ntype FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [\n { message: `\"${key}\" must be a finite number when provided`, path: [key] },\n ],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ query: string; maxResults?: number }>({\n * query: stringField(),\n * maxResults: optionalNumberField(),\n * });\n */\nexport function objectSchema<T extends object>(\n shape: ObjectShape<T>,\n): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,SAAS;;AAaf,SAAgB,cAAsC;CACpD,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAqB,MAAM,CAAC,GAAG;EAAE,CAAC,EAAE;CAC3E;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GACpD,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CACN;GAAE,SAAS,IAAI,IAAI;GAA0C,MAAM,CAAC,GAAG;EAAE,CAC3E,EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,aACd,OACqB;CACrB,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,SAAS,OAAO;GACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;GAG5D,MAAM,SAAS;GACf,MAAM,SAAmC,CAAC;GAC1C,MAAM,SAAkC,CAAC;GAEzC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAkB;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,UAAU,MAAM,OAAO,MAAgB,GAAa;IAE1D,IAAI,YAAY,SAAS;KACvB,OAAO,KAAK,GAAG,QAAQ,MAAM;KAE7B;IACF;IAKA,IAAI,QAAQ,UAAU,QACpB,OAAO,OAAiB,QAAQ;GAEpC;GAEA,IAAI,OAAO,SAAS,GAClB,OAAO,EAAE,OAAO;GAGlB,OAAO,EAAE,OAAO,OAAY;EAC9B;CACF,EACF;AACF"}
1
+ {"version":3,"file":"schema.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the web tools' input validation. The package pins only\n * `@warlock.js/ai` as a runtime peer, so rather than pull in a schema\n * library we hand-roll the few shapes the web tools need — exactly the\n * pattern `@warlock.js/ai`'s own `tool()` tests use and the sibling\n * `@warlock.js/ai-workspace` package follows. Each builder returns a\n * `StandardSchemaV1`, which is what `tool({ input })` validates against\n * before calling `execute`.\n *\n * These intentionally cover only the primitive cases the WEB tools\n * require (`string`, `optional number`, and an `object` of fields). They\n * are not a general-purpose validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-tools\";\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\ntype FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a finite number when provided`, path: [key] }],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ query: string; maxResults?: number }>({\n * query: stringField(),\n * maxResults: optionalNumberField(),\n * });\n */\nexport function objectSchema<T extends object>(shape: ObjectShape<T>): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,SAAS;;AAaf,SAAgB,cAAsC;CACpD,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAqB,MAAM,CAAC,GAAG;EAAE,CAAC,EAAE;CAC3E;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GACpD,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAA0C,MAAM,CAAC,GAAG;EAAE,CAAC,EACrF;CACF;AACF;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,aAA+B,OAA4C;CACzF,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,SAAS,OAAO;GACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;GAG5D,MAAM,SAAS;GACf,MAAM,SAAmC,CAAC;GAC1C,MAAM,SAAkC,CAAC;GAEzC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAkB;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,UAAU,MAAM,OAAO,MAAgB,GAAa;IAE1D,IAAI,YAAY,SAAS;KACvB,OAAO,KAAK,GAAG,QAAQ,MAAM;KAE7B;IACF;IAKA,IAAI,QAAQ,UAAU,QACpB,OAAO,OAAiB,QAAQ;GAEpC;GAEA,IAAI,OAAO,SAAS,GAClB,OAAO,EAAE,OAAO;GAGlB,OAAO,EAAE,OAAO,OAAY;EAC9B;CACF,EACF;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { WebSearchInput, WebSearchOptions, WebSearchResult } from "../contracts/web.type.mjs";
2
+ import "../contracts/index.mjs";
2
3
  import { ToolContract } from "@warlock.js/ai";
3
-
4
4
  //#region ../../packages/ai-tools/src/web/web-search.d.ts
5
5
  /**
6
6
  * Build the agent-facing `web_search` tool over a {@link SearchProvider}.
@@ -32,7 +32,6 @@ import { ToolContract } from "@warlock.js/ai";
32
32
  * const { data } = await search.invoke({ query: "warlock.js ai tools" });
33
33
  * for (const hit of data?.results ?? []) console.log(hit.title, hit.url);
34
34
  */
35
- declare function webSearchTool(options: WebSearchOptions): ToolContract<WebSearchInput, WebSearchResult>;
35
+ export declare function webSearchTool(options: WebSearchOptions): ToolContract<WebSearchInput, WebSearchResult>;
36
36
  //#endregion
37
- export { webSearchTool };
38
37
  //# sourceMappingURL=web-search.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"web-search.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/web-search.ts"],"mappings":";;;;;;;AAsNA;;;;;;;;;;;;;;;AAE+C;;;;;;;;;;;;iBAF/B,aAAA,CACd,OAAA,EAAS,gBAAA,GACR,YAAA,CAAa,cAAA,EAAgB,eAAA"}
1
+ {"version":3,"file":"web-search.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/web-search.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAsNgB,cACd,SAAS,mBACR,aAAa,gBAAgB"}
@@ -48,7 +48,8 @@ async function fetchJson(url, init, provider) {
48
48
  try {
49
49
  response = await fetch(url, init);
50
50
  } catch (cause) {
51
- throw new WebToolError(`web_search request to the "${provider}" provider failed: ${cause instanceof Error ? cause.message : String(cause)}`, {
51
+ const message = cause instanceof Error ? cause.message : String(cause);
52
+ throw new WebToolError(`web_search request to the "${provider}" provider failed: ${message}`, {
52
53
  type: "request-failed",
53
54
  cause
54
55
  });
@@ -153,9 +154,7 @@ function webSearchTool(options) {
153
154
  case "brave":
154
155
  results = await searchBrave(input.query, limit, apiKey);
155
156
  break;
156
- case "serpapi":
157
- results = await searchSerpApi(input.query, limit, apiKey);
158
- break;
157
+ case "serpapi": results = await searchSerpApi(input.query, limit, apiKey);
159
158
  }
160
159
  return { results };
161
160
  }
@@ -1 +1 @@
1
- {"version":3,"file":"web-search.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/web-search.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n SearchProvider,\n WebSearchInput,\n WebSearchOptions,\n WebSearchResult,\n WebSearchResultItem,\n} from \"../contracts\";\nimport { WebToolError } from \"../errors\";\nimport { objectSchema, optionalNumberField, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"web_search\";\n\n/** Default per-call result cap when the constructor does not set one. */\nconst DEFAULT_MAX_RESULTS = 5;\n\n/**\n * Per-provider configuration — the environment variable consulted when no\n * `apiKey` is passed, plus the human-readable provider label used in\n * error messages.\n */\nconst PROVIDER_ENV: Record<SearchProvider, string> = {\n tavily: \"TAVILY_API_KEY\",\n brave: \"BRAVE_API_KEY\",\n serpapi: \"SERPAPI_API_KEY\",\n};\n\n/** Input schema: `{ query, maxResults? }`. */\nconst inputSchema = objectSchema<WebSearchInput>({\n query: stringField(),\n maxResults: optionalNumberField(),\n});\n\n/**\n * Resolve the API key from explicit options or the provider's environment\n * variable, throwing a typed {@link WebToolError} when neither is present.\n */\nfunction resolveApiKey(provider: SearchProvider, apiKey?: string): string {\n const key = apiKey ?? process.env[PROVIDER_ENV[provider]];\n\n if (!key) {\n throw new WebToolError(\n `web_search requires an API key for the \"${provider}\" provider. ` +\n `Pass { apiKey } or set the ${PROVIDER_ENV[provider]} environment variable.`,\n { type: \"missing-key\" },\n );\n }\n\n return key;\n}\n\n/**\n * Clamp the model-requested result count into `[1, max]`. An omitted /\n * non-positive request falls back to the configured default.\n */\nfunction clampResults(requested: number | undefined, max: number): number {\n if (requested === undefined || requested < 1) {\n return Math.min(DEFAULT_MAX_RESULTS, max);\n }\n\n return Math.min(Math.max(1, Math.floor(requested)), max);\n}\n\n/**\n * Shape of the relevant slice of a Tavily `/search` response. Tavily\n * returns LLM-ready `content` snippets and a relevance `score` per hit.\n */\ninterface TavilyResponse {\n results?: Array<{ title?: string; url?: string; content?: string; score?: number }>;\n}\n\n/** Shape of the relevant slice of a Brave web-search response. */\ninterface BraveResponse {\n web?: { results?: Array<{ title?: string; url?: string; description?: string }> };\n}\n\n/** Shape of the relevant slice of a SerpAPI `search.json` response. */\ninterface SerpApiResponse {\n organic_results?: Array<{ title?: string; link?: string; snippet?: string }>;\n}\n\n/**\n * Issue the provider HTTP call and return its parsed JSON, mapping a\n * non-OK status or a network failure to a typed {@link WebToolError}.\n */\nasync function fetchJson(\n url: string,\n init: RequestInit,\n provider: SearchProvider,\n): Promise<unknown> {\n let response: Response;\n\n try {\n response = await fetch(url, init);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new WebToolError(\n `web_search request to the \"${provider}\" provider failed: ${message}`,\n { type: \"request-failed\", cause },\n );\n }\n\n if (!response.ok) {\n throw new WebToolError(\n `web_search \"${provider}\" provider returned HTTP ${response.status}.`,\n { type: \"request-failed\", context: { status: response.status } },\n );\n }\n\n return response.json();\n}\n\n/** Drive Tavily's `/search` HTTP API and normalize its hits. */\nasync function searchTavily(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const json = (await fetchJson(\n \"https://api.tavily.com/search\",\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, query, max_results: maxResults }),\n },\n \"tavily\",\n )) as TavilyResponse;\n\n return (json.results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.url ?? \"\",\n snippet: hit.content ?? \"\",\n score: hit.score,\n }));\n}\n\n/** Drive Brave's web-search HTTP API and normalize its hits. */\nasync function searchBrave(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const url = new URL(\"https://api.search.brave.com/res/v1/web/search\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"count\", String(maxResults));\n\n const json = (await fetchJson(\n url.toString(),\n {\n method: \"GET\",\n headers: { accept: \"application/json\", \"x-subscription-token\": apiKey },\n },\n \"brave\",\n )) as BraveResponse;\n\n return (json.web?.results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.url ?? \"\",\n snippet: hit.description ?? \"\",\n }));\n}\n\n/** Drive SerpAPI's `search.json` HTTP API and normalize its hits. */\nasync function searchSerpApi(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const url = new URL(\"https://serpapi.com/search.json\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"num\", String(maxResults));\n url.searchParams.set(\"api_key\", apiKey);\n\n const json = (await fetchJson(url.toString(), { method: \"GET\" }, \"serpapi\")) as SerpApiResponse;\n\n return (json.organic_results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.link ?? \"\",\n snippet: hit.snippet ?? \"\",\n }));\n}\n\n/**\n * Build the agent-facing `web_search` tool over a {@link SearchProvider}.\n *\n * The provider's HTTP API is called directly through the global `fetch`\n * (Node 18+) — Tavily via `POST /search` (LLM-ready snippets + relevance\n * scores), Brave and SerpAPI via their `GET` endpoints. The API key is\n * taken from `options.apiKey`, falling back to the provider's environment\n * variable (`TAVILY_API_KEY` / `BRAVE_API_KEY` / `SERPAPI_API_KEY`). No\n * provider SDK is required for the HTTP path; `@tavily/core` remains an\n * optional peer for callers who prefer it, but this factory never forces\n * it to be installed.\n *\n * The model passes `{ query, maxResults? }`; `maxResults` is clamped into\n * `[1, options.maxResults]` (default {@link DEFAULT_MAX_RESULTS}).\n *\n * **Errors flow as data.** A missing key, a non-OK provider status, or a\n * network failure throws a typed {@link WebToolError}; the `tool()`\n * wrapper catches it and surfaces it in the returned `{ error }` field —\n * `invoke()` never throws — so the agent can read the failure and\n * self-correct.\n *\n * @param options - Provider selection, API key, result cap, and an\n * optional tool-name override.\n * @returns A `ToolContract<{ query; maxResults? }, WebSearchResult>`.\n *\n * @example\n * const search = webSearchTool({ provider: \"tavily\" });\n * const { data } = await search.invoke({ query: \"warlock.js ai tools\" });\n * for (const hit of data?.results ?? []) console.log(hit.title, hit.url);\n */\nexport function webSearchTool(\n options: WebSearchOptions,\n): ToolContract<WebSearchInput, WebSearchResult> {\n const provider = options.provider;\n const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;\n\n return tool<WebSearchInput, WebSearchResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Search the web for current information and return a ranked list of \" +\n \"results (title, URL, and a short snippet). Use for facts that may have \" +\n \"changed since training, or anything you are unsure about.\",\n input: inputSchema,\n async execute(input) {\n const apiKey = resolveApiKey(provider, options.apiKey);\n const limit = clampResults(input.maxResults, maxResults);\n\n let results: WebSearchResultItem[];\n\n switch (provider) {\n case \"tavily\":\n results = await searchTavily(input.query, limit, apiKey);\n break;\n case \"brave\":\n results = await searchBrave(input.query, limit, apiKey);\n break;\n case \"serpapi\":\n results = await searchSerpApi(input.query, limit, apiKey);\n break;\n }\n\n return { results };\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"web-search.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/web/web-search.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n SearchProvider,\n WebSearchInput,\n WebSearchOptions,\n WebSearchResult,\n WebSearchResultItem,\n} from \"../contracts\";\nimport { WebToolError } from \"../errors\";\nimport { objectSchema, optionalNumberField, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"web_search\";\n\n/** Default per-call result cap when the constructor does not set one. */\nconst DEFAULT_MAX_RESULTS = 5;\n\n/**\n * Per-provider configuration — the environment variable consulted when no\n * `apiKey` is passed, plus the human-readable provider label used in\n * error messages.\n */\nconst PROVIDER_ENV: Record<SearchProvider, string> = {\n tavily: \"TAVILY_API_KEY\",\n brave: \"BRAVE_API_KEY\",\n serpapi: \"SERPAPI_API_KEY\",\n};\n\n/** Input schema: `{ query, maxResults? }`. */\nconst inputSchema = objectSchema<WebSearchInput>({\n query: stringField(),\n maxResults: optionalNumberField(),\n});\n\n/**\n * Resolve the API key from explicit options or the provider's environment\n * variable, throwing a typed {@link WebToolError} when neither is present.\n */\nfunction resolveApiKey(provider: SearchProvider, apiKey?: string): string {\n const key = apiKey ?? process.env[PROVIDER_ENV[provider]];\n\n if (!key) {\n throw new WebToolError(\n `web_search requires an API key for the \"${provider}\" provider. ` +\n `Pass { apiKey } or set the ${PROVIDER_ENV[provider]} environment variable.`,\n { type: \"missing-key\" },\n );\n }\n\n return key;\n}\n\n/**\n * Clamp the model-requested result count into `[1, max]`. An omitted /\n * non-positive request falls back to the configured default.\n */\nfunction clampResults(requested: number | undefined, max: number): number {\n if (requested === undefined || requested < 1) {\n return Math.min(DEFAULT_MAX_RESULTS, max);\n }\n\n return Math.min(Math.max(1, Math.floor(requested)), max);\n}\n\n/**\n * Shape of the relevant slice of a Tavily `/search` response. Tavily\n * returns LLM-ready `content` snippets and a relevance `score` per hit.\n */\ninterface TavilyResponse {\n results?: Array<{ title?: string; url?: string; content?: string; score?: number }>;\n}\n\n/** Shape of the relevant slice of a Brave web-search response. */\ninterface BraveResponse {\n web?: { results?: Array<{ title?: string; url?: string; description?: string }> };\n}\n\n/** Shape of the relevant slice of a SerpAPI `search.json` response. */\ninterface SerpApiResponse {\n organic_results?: Array<{ title?: string; link?: string; snippet?: string }>;\n}\n\n/**\n * Issue the provider HTTP call and return its parsed JSON, mapping a\n * non-OK status or a network failure to a typed {@link WebToolError}.\n */\nasync function fetchJson(\n url: string,\n init: RequestInit,\n provider: SearchProvider,\n): Promise<unknown> {\n let response: Response;\n\n try {\n response = await fetch(url, init);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n throw new WebToolError(`web_search request to the \"${provider}\" provider failed: ${message}`, {\n type: \"request-failed\",\n cause,\n });\n }\n\n if (!response.ok) {\n throw new WebToolError(`web_search \"${provider}\" provider returned HTTP ${response.status}.`, {\n type: \"request-failed\",\n context: { status: response.status },\n });\n }\n\n return response.json();\n}\n\n/** Drive Tavily's `/search` HTTP API and normalize its hits. */\nasync function searchTavily(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const json = (await fetchJson(\n \"https://api.tavily.com/search\",\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ api_key: apiKey, query, max_results: maxResults }),\n },\n \"tavily\",\n )) as TavilyResponse;\n\n return (json.results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.url ?? \"\",\n snippet: hit.content ?? \"\",\n score: hit.score,\n }));\n}\n\n/** Drive Brave's web-search HTTP API and normalize its hits. */\nasync function searchBrave(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const url = new URL(\"https://api.search.brave.com/res/v1/web/search\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"count\", String(maxResults));\n\n const json = (await fetchJson(\n url.toString(),\n {\n method: \"GET\",\n headers: { accept: \"application/json\", \"x-subscription-token\": apiKey },\n },\n \"brave\",\n )) as BraveResponse;\n\n return (json.web?.results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.url ?? \"\",\n snippet: hit.description ?? \"\",\n }));\n}\n\n/** Drive SerpAPI's `search.json` HTTP API and normalize its hits. */\nasync function searchSerpApi(\n query: string,\n maxResults: number,\n apiKey: string,\n): Promise<WebSearchResultItem[]> {\n const url = new URL(\"https://serpapi.com/search.json\");\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"num\", String(maxResults));\n url.searchParams.set(\"api_key\", apiKey);\n\n const json = (await fetchJson(url.toString(), { method: \"GET\" }, \"serpapi\")) as SerpApiResponse;\n\n return (json.organic_results ?? []).slice(0, maxResults).map((hit) => ({\n title: hit.title ?? \"\",\n url: hit.link ?? \"\",\n snippet: hit.snippet ?? \"\",\n }));\n}\n\n/**\n * Build the agent-facing `web_search` tool over a {@link SearchProvider}.\n *\n * The provider's HTTP API is called directly through the global `fetch`\n * (Node 18+) — Tavily via `POST /search` (LLM-ready snippets + relevance\n * scores), Brave and SerpAPI via their `GET` endpoints. The API key is\n * taken from `options.apiKey`, falling back to the provider's environment\n * variable (`TAVILY_API_KEY` / `BRAVE_API_KEY` / `SERPAPI_API_KEY`). No\n * provider SDK is required for the HTTP path; `@tavily/core` remains an\n * optional peer for callers who prefer it, but this factory never forces\n * it to be installed.\n *\n * The model passes `{ query, maxResults? }`; `maxResults` is clamped into\n * `[1, options.maxResults]` (default {@link DEFAULT_MAX_RESULTS}).\n *\n * **Errors flow as data.** A missing key, a non-OK provider status, or a\n * network failure throws a typed {@link WebToolError}; the `tool()`\n * wrapper catches it and surfaces it in the returned `{ error }` field —\n * `invoke()` never throws — so the agent can read the failure and\n * self-correct.\n *\n * @param options - Provider selection, API key, result cap, and an\n * optional tool-name override.\n * @returns A `ToolContract<{ query; maxResults? }, WebSearchResult>`.\n *\n * @example\n * const search = webSearchTool({ provider: \"tavily\" });\n * const { data } = await search.invoke({ query: \"warlock.js ai tools\" });\n * for (const hit of data?.results ?? []) console.log(hit.title, hit.url);\n */\nexport function webSearchTool(\n options: WebSearchOptions,\n): ToolContract<WebSearchInput, WebSearchResult> {\n const provider = options.provider;\n const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;\n\n return tool<WebSearchInput, WebSearchResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Search the web for current information and return a ranked list of \" +\n \"results (title, URL, and a short snippet). Use for facts that may have \" +\n \"changed since training, or anything you are unsure about.\",\n input: inputSchema,\n async execute(input) {\n const apiKey = resolveApiKey(provider, options.apiKey);\n const limit = clampResults(input.maxResults, maxResults);\n\n let results: WebSearchResultItem[];\n\n switch (provider) {\n case \"tavily\":\n results = await searchTavily(input.query, limit, apiKey);\n break;\n case \"brave\":\n results = await searchBrave(input.query, limit, apiKey);\n break;\n case \"serpapi\":\n results = await searchSerpApi(input.query, limit, apiKey);\n break;\n }\n\n return { results };\n },\n });\n}\n"],"mappings":"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"}