jev-recipes 0.9.2 → 0.9.6

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.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![Node.js version](https://img.shields.io/node/v/jev-recipes)](https://nodejs.org/)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/agencyenterprise/jev-recipes/blob/main/LICENSE)
9
9
 
10
- [Quickstart](#use-a-recipe) | [Live music app](https://jev-ai-music.com/) | [Coding assistants](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/coding-assistants.md) | [Recipe catalog](https://github.com/agencyenterprise/jev-recipes/blob/main/recipes/README.md) | [API vs. SDK vs. recipes](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/api-sdk-recipes.md) | [Agent frameworks](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/framework-adapters.md) | [Contributing](https://github.com/agencyenterprise/jev-recipes/blob/main/CONTRIBUTING.md)
10
+ [Recipe guides at jev-recipes.com](https://jev-recipes.com/) | [Quickstart](#use-a-recipe) | [Live music app](https://jev-ai-music.com/) | [Coding assistants](https://jev-recipes.com/docs/coding-assistants/) | [Recipe catalog](https://jev-recipes.com/recipes/) | [API vs. SDK vs. recipes](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/api-sdk-recipes.md) | [Agent frameworks](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/framework-adapters.md) | [Contributing](https://github.com/agencyenterprise/jev-recipes/blob/main/CONTRIBUTING.md)
11
11
 
12
12
  <!-- BEGIN GENERATED: summary -->
13
13
 
@@ -15,21 +15,35 @@
15
15
 
16
16
  <!-- END GENERATED: summary -->
17
17
 
18
+ ## Documentation versions
19
+
20
+ Run `npm ls jev-recipes` in your project to check the version you installed, then choose its `v<version>` tag from [all release tags](https://github.com/agencyenterprise/jev-recipes/tags) to browse matching documentation.
21
+
22
+ The hosted catalog and links to `main` can include unreleased changes. See [release notes](https://github.com/agencyenterprise/jev-recipes/releases) for compatibility and migration changes.
23
+
24
+ ## Help and contributions
25
+
26
+ [Ask a usage question](https://github.com/agencyenterprise/jev-recipes/issues/new?template=usage-question.yml), [report a software bug](https://github.com/agencyenterprise/jev-recipes/issues/new?template=bug-report.yml), [share an unexpected model decision](https://github.com/agencyenterprise/jev-recipes/issues/new?template=model-decision.yml), or [request a recipe](https://github.com/agencyenterprise/jev-recipes/issues/new?template=recipe-request.yml).
27
+
28
+ For a reproducible software problem, start with the [standalone reproduction example](https://github.com/agencyenterprise/jev-recipes/tree/main/examples/reproduction). Share sanitized inputs and logs, never credentials or private data. To help improve the project, see [your first contribution](https://github.com/agencyenterprise/jev-recipes/blob/main/CONTRIBUTING.md#your-first-contribution).
29
+
30
+ ## How it works
31
+
18
32
  Each recipe accepts your data, calls [Jev through TypeSafe's System One API](https://docs.typesafe.ai/introduction) or an injected compatible client, and returns a structured decision. Use it in a Node.js backend, a script, or a research evaluation. Your application decides what happens next.
19
33
 
20
34
  ## Start with one decision
21
35
 
22
- | Your task | Try without a key | Build the workflow |
23
- | --------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
24
- | Route work to the right team | `npx jev-recipes demo route` | [Route work](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/getting-started/README.md#route-work) |
25
- | Select useful evidence | `npx jev-recipes demo rerank` | [Select evidence](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/getting-started/README.md#select-evidence) |
26
- | Review an agent's proposed action | `npx jev-recipes demo tool-call-gate` | [Review an action](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/getting-started/README.md#review-an-action) |
36
+ | Your task | Recipe guide | Try without a key | Build the workflow |
37
+ | --------------------------------- | ------------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
38
+ | Route work to the right team | [`route`](https://jev-recipes.com/recipes/route/) | `npx jev-recipes demo route` | [Route work](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/getting-started/README.md#route-work) |
39
+ | Select useful evidence | [`rerank`](https://jev-recipes.com/recipes/rerank/) | `npx jev-recipes demo rerank` | [Select evidence](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/getting-started/README.md#select-evidence) |
40
+ | Review an agent's proposed action | [`tool-call-gate`](https://jev-recipes.com/recipes/tool-call-gate/) | `npx jev-recipes demo tool-call-gate` | [Review an action](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/getting-started/README.md#review-an-action) |
27
41
 
28
42
  These demos use saved responses. The [runnable workflow examples](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/getting-started/README.md) show ready decisions, uncertainty, and provider failures. Use [TypeSafe directly or Vercel Gateway](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/integrations.md) for live calls.
29
43
 
30
44
  For example, give `route` a support message and descriptions of your teams. It returns a team such as `billing`, or a review outcome when the choice is uncertain.
31
45
 
32
- Evaluate your own cases with the [installed evaluator](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/evaluation.md), retain model responses, and replay confidence policies offline. The [agent workflow](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/agent-loop/README.md) and [customer queue](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/customer-queue/README.md) show how decisions fit into application code. [Direct and Gateway integrations](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/integrations.md) use the same recipe interface.
46
+ Evaluate your own cases with the [installed evaluator](https://jev-recipes.com/docs/evaluation/), retain model responses, and replay confidence policies offline. The [agent workflow](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/agent-loop/README.md) and [customer queue](https://github.com/agencyenterprise/jev-recipes/blob/main/examples/customer-queue/README.md) show how decisions fit into application code. [Direct and Gateway integrations](https://github.com/agencyenterprise/jev-recipes/blob/main/docs/integrations.md) use the same recipe interface.
33
47
 
34
48
  ## Watch Jev play checkers against Jev using the checkers-move recipe
35
49
 
@@ -1,8 +1,8 @@
1
- import type { LanguageModel, PrepareStepFunction, StopCondition, Tool, ToolSet } from 'ai';
1
+ import type { FlexibleSchema, LanguageModel, PrepareStepFunction, StopCondition, Tool, ToolSet } from 'ai';
2
2
  import type { RecipeName } from '../../catalog/schema.js';
3
3
  import type { ModelRouteResult } from '../../recipes/model-route/index.js';
4
4
  import type { RecipeOptions } from '../../src/schema.js';
5
- import type { CompletionOptions, CompletionToolOutput, GuardOptions, RecipeToolOptions } from '../shared.js';
5
+ import type { BlockedToolOutput, CompletionOptions, CompletionToolOutput, GuardOptions, RecipeToolOptions } from '../shared.js';
6
6
  export type { BlockedToolOutput, CompletionEvent, CompletionOptions, CompletionToolOutput, GuardEvent, GuardOptions, RecipeToolOptions, } from '../shared.js';
7
7
  export type RecipeTool = Tool<Record<string, unknown>, Record<string, unknown>>;
8
8
  export declare function recipeTool(name: RecipeName, options?: RecipeToolOptions): RecipeTool;
@@ -10,7 +10,20 @@ export declare function recipeTools(names: readonly RecipeName[], options?: Reci
10
10
  export type GuardToolsOptions = GuardOptions & {
11
11
  request?: string;
12
12
  };
13
- export declare function guardTools<TOOLS extends ToolSet>(tools: TOOLS, options?: GuardToolsOptions): TOOLS;
13
+ type ToolExecution<DEFINITION extends ToolSet[string]> = NonNullable<DEFINITION['execute']>;
14
+ type ToolOutput<RESULT> = RESULT extends AsyncIterable<infer OUTPUT> ? OUTPUT : Awaited<RESULT>;
15
+ type GuardedOutput<DEFINITION extends ToolSet[string]> = ToolOutput<ReturnType<ToolExecution<DEFINITION>>> | BlockedToolOutput;
16
+ type GuardedExecution<DEFINITION extends ToolSet[string]> = {
17
+ [KEY in keyof Pick<DEFINITION, 'execute'>]: (...args: Parameters<ToolExecution<DEFINITION>>) => AsyncGenerator<GuardedOutput<DEFINITION>>;
18
+ };
19
+ type GuardedTool<DEFINITION extends ToolSet[string]> = DEFINITION extends unknown ? Omit<DEFINITION, 'execute' | 'toModelOutput' | 'outputSchema'> & GuardedExecution<DEFINITION> & {
20
+ outputSchema?: FlexibleSchema<GuardedOutput<DEFINITION>>;
21
+ toModelOutput?: NonNullable<Tool<Parameters<ToolExecution<DEFINITION>>[0], GuardedOutput<DEFINITION>>['toModelOutput']>;
22
+ } : never;
23
+ export type GuardedTools<TOOLS extends ToolSet> = {
24
+ [NAME in keyof TOOLS]: 'execute' extends keyof TOOLS[NAME] ? ToolExecution<TOOLS[NAME]> extends never ? TOOLS[NAME] : GuardedTool<TOOLS[NAME]> : TOOLS[NAME];
25
+ };
26
+ export declare function guardTools<TOOLS extends ToolSet>(tools: TOOLS, options?: GuardToolsOptions): GuardedTools<TOOLS>;
14
27
  export type ModelCandidate = {
15
28
  id: string;
16
29
  text: string;
@@ -1,6 +1,8 @@
1
- import { jsonSchema, tool } from 'ai';
1
+ import { asSchema, jsonSchema, tool } from 'ai';
2
+ import { z } from 'zod';
2
3
  import { modelRoute } from '../../recipes/model-route/index.js';
3
- import { COMPLETION_TOOL_DESCRIPTION, COMPLETION_TOOL_INPUT_SCHEMA, DEFAULT_COMPLETION_TOOL_NAME, blockedOutput, defineRecipeTool, messageText, requireText, reviewCompletion, reviewToolCall, } from '../shared.js';
4
+ import { toolCallGateResultSchema } from '../../recipes/tool-call-gate/index.js';
5
+ import { BLOCKED_TOOL_OUTPUT_KIND, COMPLETION_TOOL_DESCRIPTION, COMPLETION_TOOL_INPUT_SCHEMA, DEFAULT_COMPLETION_TOOL_NAME, blockedOutput, defineRecipeTool, messageText, requireText, reviewCompletion, reviewToolCall, } from '../shared.js';
4
6
  export function recipeTool(name, options = {}) {
5
7
  const definition = defineRecipeTool(name, options);
6
8
  return tool({
@@ -13,27 +15,75 @@ export function recipeTools(names, options = {}) {
13
15
  return Object.fromEntries(names.map((name) => [name, recipeTool(name, options)]));
14
16
  }
15
17
  export function guardTools(tools, options = {}) {
18
+ return Object.fromEntries(Object.entries(tools).map(([name, definition]) => [name, guardTool(name, definition, options)]));
19
+ }
20
+ function guardTool(toolName, definition, options) {
21
+ const execute = definition.execute;
22
+ if (typeof execute !== 'function')
23
+ return definition;
16
24
  const { request, ...guardOptions } = options;
17
- const guarded = Object.entries(tools).map(([toolName, definition]) => {
18
- if (!('execute' in definition) || typeof definition.execute !== 'function')
19
- return [toolName, definition];
20
- const execute = definition.execute;
21
- const wrapped = {
22
- ...definition,
23
- execute: async (input, executionOptions) => {
24
- const decision = await reviewToolCall({
25
- toolName,
26
- input,
27
- request: request ?? requireText(lastUserText(executionOptions.messages), 'request'),
28
- }, guardOptions, executionOptions.abortSignal);
29
- if (decision.action !== 'allow')
30
- return blockedOutput(toolName, decision);
31
- return execute(input, executionOptions);
32
- },
33
- };
34
- return [toolName, wrapped];
25
+ const toModelOutput = definition.toModelOutput;
26
+ return {
27
+ ...definition,
28
+ execute: async function* (input, executionOptions) {
29
+ guardOptions.signal?.throwIfAborted();
30
+ executionOptions.abortSignal?.throwIfAborted();
31
+ const decision = await reviewToolCall({
32
+ toolName,
33
+ input,
34
+ request: request ?? requireText(lastUserText(executionOptions.messages), 'request'),
35
+ }, guardOptions, executionOptions.abortSignal);
36
+ guardOptions.signal?.throwIfAborted();
37
+ executionOptions.abortSignal?.throwIfAborted();
38
+ if (decision.action !== 'allow') {
39
+ yield blockedOutput(toolName, decision);
40
+ return;
41
+ }
42
+ const output = execute(input, executionOptions);
43
+ if (isAsyncIterable(output))
44
+ yield* output;
45
+ else
46
+ yield await output;
47
+ },
48
+ ...(toModelOutput === undefined
49
+ ? {}
50
+ : {
51
+ toModelOutput: (result) => blockedToolOutputSchema.safeParse(result.output).success
52
+ ? { type: 'json', value: result.output }
53
+ : toModelOutput(result),
54
+ }),
55
+ ...(definition.outputSchema === undefined
56
+ ? {}
57
+ : { outputSchema: includeBlockedOutput(definition.outputSchema) }),
58
+ };
59
+ }
60
+ function isAsyncIterable(value) {
61
+ return (typeof value?.[Symbol.asyncIterator] ===
62
+ 'function');
63
+ }
64
+ const blockedToolOutputSchema = toolCallGateResultSchema
65
+ .pick({ verdict: true, status: true, detected: true, confidence: true })
66
+ .extend({
67
+ kind: z.literal(BLOCKED_TOOL_OUTPUT_KIND),
68
+ blocked: z.literal(true),
69
+ action: z.enum(['ask', 'deny']),
70
+ message: z.string(),
71
+ });
72
+ function includeBlockedOutput(outputSchema) {
73
+ const original = asSchema(outputSchema);
74
+ return jsonSchema(async () => ({
75
+ anyOf: [
76
+ { $id: 'urn:jev-recipes:tool-output', ...(await original.jsonSchema) },
77
+ z.toJSONSchema(blockedToolOutputSchema),
78
+ ],
79
+ }), {
80
+ validate: async (value) => {
81
+ const blocked = blockedToolOutputSchema.safeParse(value);
82
+ if (blocked.success)
83
+ return { success: true, value: blocked.data };
84
+ return original.validate ? original.validate(value) : { success: true, value };
85
+ },
35
86
  });
36
- return Object.fromEntries(guarded);
37
87
  }
38
88
  export function routeModelStep(options) {
39
89
  const { candidates, request, context, minConfidence, fallback, onDecision, ...recipeOptions } = options;
@@ -1,11 +1,4 @@
1
- import type { DynamicStructuredTool } from '@langchain/core/tools';
2
- export type GuardableTool = {
3
- name: string;
4
- description: string;
5
- schema: unknown;
6
- returnDirect?: boolean;
7
- invoke(input: any, config?: any): Promise<unknown>;
8
- };
1
+ import { DynamicStructuredTool } from '@langchain/core/tools';
9
2
  import type { RecipeName } from '../../catalog/schema.js';
10
3
  import { COMPLETION_TOOL_INPUT_SCHEMA } from '../shared.js';
11
4
  import type { CompletionOptions, GuardOptions, RecipeToolOptions } from '../shared.js';
@@ -18,6 +11,13 @@ export type GuardRequest = string | ((input: unknown, config: unknown) => string
18
11
  export type GuardToolsOptions = GuardOptions & {
19
12
  request: GuardRequest;
20
13
  };
14
+ export type GuardableTool = {
15
+ name: string;
16
+ description: string;
17
+ schema: unknown;
18
+ returnDirect?: boolean;
19
+ invoke(input: any, config?: any): Promise<unknown>;
20
+ };
21
21
  export declare function guardTools(tools: readonly GuardableTool[], options: GuardToolsOptions): DynamicStructuredTool[];
22
22
  export declare function completionTool(options: CompletionOptions & {
23
23
  task: string;
@@ -1,4 +1,5 @@
1
- import { tool } from '@langchain/core/tools';
1
+ import { DynamicStructuredTool, tool } from '@langchain/core/tools';
2
+ import { ToolMessage } from '@langchain/core/messages';
2
3
  import { COMPLETION_TOOL_DESCRIPTION, COMPLETION_TOOL_INPUT_SCHEMA, DEFAULT_COMPLETION_TOOL_NAME, blockedOutput, defineRecipeTool, requireText, reviewCompletion, reviewToolCall, } from '../shared.js';
3
4
  export function recipeTool(name, options = {}) {
4
5
  const definition = defineRecipeTool(name, options);
@@ -12,26 +13,53 @@ export function recipeTools(names, options = {}) {
12
13
  return names.map((name) => recipeTool(name, options));
13
14
  }
14
15
  export function guardTools(tools, options) {
15
- const { request, ...guardOptions } = options;
16
- return tools.map((original) => {
17
- const schema = original.schema;
18
- return tool(async (input, config) => {
19
- const decision = await reviewToolCall({
20
- toolName: original.name,
21
- input,
22
- request: typeof request === 'function' ? request(input, config) : request,
23
- }, guardOptions, config?.signal);
24
- if (decision.action !== 'allow')
25
- return blockedOutput(original.name, decision);
26
- const { toolCall: _toolCall, ...delegatedConfig } = config ?? {};
27
- return original.invoke(input, delegatedConfig);
28
- }, {
16
+ return tools.map((original) => new GuardedTool(original, options));
17
+ }
18
+ class GuardedTool extends DynamicStructuredTool {
19
+ original;
20
+ guardOptions;
21
+ constructor(original, guardOptions) {
22
+ super({
29
23
  name: original.name,
30
24
  description: original.description,
31
- schema,
32
- ...(original.returnDirect === undefined ? {} : { returnDirect: original.returnDirect }),
25
+ schema: original.schema,
26
+ returnDirect: original.returnDirect ?? false,
27
+ func: (input, _runManager, config) => this.invoke(input, config),
33
28
  });
34
- });
29
+ this.original = original;
30
+ this.guardOptions = guardOptions;
31
+ }
32
+ async invoke(input, config) {
33
+ const { request, ...options } = this.guardOptions;
34
+ const args = isToolCall(input) ? input.args : input;
35
+ const requestConfig = isToolCall(input) ? { ...config, toolCall: input } : config;
36
+ options.signal?.throwIfAborted();
37
+ config?.signal?.throwIfAborted();
38
+ const decision = await reviewToolCall({
39
+ toolName: this.name,
40
+ input: args,
41
+ request: typeof request === 'function' ? request(args, requestConfig) : request,
42
+ }, options, config?.signal);
43
+ options.signal?.throwIfAborted();
44
+ config?.signal?.throwIfAborted();
45
+ if (decision.action === 'allow')
46
+ return this.original.invoke(input, config);
47
+ const output = blockedOutput(this.name, decision);
48
+ const toolCallId = requestConfig?.toolCall?.id;
49
+ return toolCallId === undefined
50
+ ? output
51
+ : new ToolMessage({
52
+ name: this.name,
53
+ tool_call_id: toolCallId,
54
+ content: JSON.stringify(output),
55
+ });
56
+ }
57
+ call(input, config, tags) {
58
+ return this.invoke(input, tags === undefined ? config : { ...config, tags: config?.tags ?? tags });
59
+ }
60
+ }
61
+ function isToolCall(input) {
62
+ return (typeof input === 'object' && input !== null && 'type' in input && input.type === 'tool_call');
35
63
  }
36
64
  export function completionTool(options) {
37
65
  return tool((input, config) => reviewCompletion({ task: requireText(options.task, 'task'), report: input.report, evidence: input.evidence }, options, config?.signal), {
@@ -22,7 +22,9 @@ export type GuardEvent = {
22
22
  input: unknown;
23
23
  decision: ToolCallGateResult;
24
24
  };
25
+ export declare const BLOCKED_TOOL_OUTPUT_KIND = "jev-recipes/blocked-tool-output";
25
26
  export type BlockedToolOutput = {
27
+ kind: typeof BLOCKED_TOOL_OUTPUT_KIND;
26
28
  blocked: true;
27
29
  action: 'ask' | 'deny';
28
30
  verdict: ToolCallGateResult['verdict'];
@@ -2,6 +2,7 @@ import { describeRecipe } from '../catalog/index.js';
2
2
  import { loadRecipe } from '../catalog/runner.js';
3
3
  import { completionGate } from '../recipes/completion-gate/index.js';
4
4
  import { toolCallGate } from '../recipes/tool-call-gate/index.js';
5
+ export const BLOCKED_TOOL_OUTPUT_KIND = 'jev-recipes/blocked-tool-output';
5
6
  export const DEFAULT_COMPLETION_TOOL_NAME = 'report_completion';
6
7
  export const COMPLETION_TOOL_DESCRIPTION = 'Report that the task is complete. State what was done in report, and put the evidence that demonstrates it, such as test output, command results, or observed behavior, in evidence. A claim without evidence is not accepted.';
7
8
  export const COMPLETION_TOOL_INPUT_SCHEMA = {
@@ -54,6 +55,7 @@ export function blockedOutput(toolName, decision) {
54
55
  ? `The call to ${toolName} was refused by policy and did not run.${risks} Do not retry it. Explain what you intended and continue with permitted work.`
55
56
  : `The call to ${toolName} did not run because a person must confirm it first.${risks} Do not retry it unattended. Explain what you intended and continue with other permitted work.`;
56
57
  return {
58
+ kind: BLOCKED_TOOL_OUTPUT_KIND,
57
59
  blocked: true,
58
60
  action,
59
61
  verdict: decision.verdict,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-recipes",
3
- "version": "0.9.2",
3
+ "version": "0.9.6",
4
4
  "description": "248 small Jev decisions for JavaScript and TypeScript: route, rerank, gate, grade, compare, and label text for agents, RAG, support, code review, and music. Each returns a typed result with explicit uncertainty. Plug into Vercel AI SDK or LangChain agents, import one recipe, or run the CLI with JSON from any language.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1068,6 +1068,8 @@
1068
1068
  "jev-recipes": "node --env-file-if-exists=.env dist/cli/index.js",
1069
1069
  "ci": "node scripts/ci.mjs",
1070
1070
  "pack:check": "node scripts/pack-check.mjs",
1071
+ "check:consumer": "node scripts/check-consumer.mjs",
1072
+ "check:reproduction": "node scripts/check-reproduction.mjs",
1071
1073
  "prepack": "npm run generate:check && npm run build",
1072
1074
  "prepublishOnly": "npm run ci",
1073
1075
  "test": "npm run test:recipes && npm run test:tooling",
@@ -1084,8 +1086,8 @@
1084
1086
  "zod": "^4.6.5"
1085
1087
  },
1086
1088
  "peerDependencies": {
1087
- "ai": ">=7.0.0",
1088
- "@langchain/core": ">=1.0.0"
1089
+ "@langchain/core": ">=1.0.0",
1090
+ "ai": ">=7.0.0"
1089
1091
  },
1090
1092
  "peerDependenciesMeta": {
1091
1093
  "ai": {
@@ -1100,6 +1102,7 @@
1100
1102
  "@types/node": "^22.0.0",
1101
1103
  "@vitest/coverage-v8": "4.1.11",
1102
1104
  "ai": "^7.0.122",
1105
+ "marked": "^18.0.14",
1103
1106
  "prettier": "3.9.8",
1104
1107
  "rimraf": "6.1.3",
1105
1108
  "typescript": "~5.9.3",