@yesilsci/health-ai-api 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yesil Health
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # @yesilsci/health-ai-api
2
+
3
+ Official TypeScript client for the **Yesil Health enterprise (B2B) API**.
4
+
5
+ Zero runtime dependencies — uses the platform `fetch` + `ReadableStream`
6
+ (Node ≥ 18, modern browsers, edge runtimes). The hard part of integrating —
7
+ parsing the typed Server-Sent Events stream — is handled for you.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @yesilsci/health-ai-api
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { YesilHealthClient } from '@yesilsci/health-ai-api';
19
+ import { randomUUID } from 'node:crypto';
20
+
21
+ const client = new YesilHealthClient({
22
+ baseUrl: 'https://api.yesilhealth.com',
23
+ apiKey: process.env.YESIL_API_KEY!, // sent as X-API-Key
24
+ });
25
+
26
+ for await (const ev of client.chatStream({
27
+ question: 'Is metformin safe for a patient with stage 3 CKD?',
28
+ request_id: randomUUID(), // idempotent billing — strongly recommended
29
+ demographics: { age: 62, sex: 'female' },
30
+ health_context: 'eGFR 45 mL/min/1.73m². On lisinopril 10mg daily.',
31
+ })) {
32
+ if (ev.type === 'delta') process.stdout.write(ev.content);
33
+ }
34
+ ```
35
+
36
+ One-shot (no live rendering):
37
+
38
+ ```ts
39
+ const { text, events } = await client.chat({ question: '...' });
40
+ ```
41
+
42
+ ## Authentication
43
+
44
+ Every request is authenticated with your enterprise API key via the `X-API-Key`
45
+ header. Keep it server-side; never ship it in a browser bundle or mobile app.
46
+
47
+ ## The event stream
48
+
49
+ `chatStream()` yields typed events. Route on `event.type`:
50
+
51
+ | `type` | Payload | Notes |
52
+ |--------------------|--------------------------------------|-------|
53
+ | `meta` | `phase`, `conversation_id`, research preview fields | Lifecycle + mid-stream research previews. |
54
+ | `delta` | `content: string` | Append `content` to build the answer text. |
55
+ | `citation` | citation fields | A literature citation for the answer. |
56
+ | `graph` | `data: {...}` | Structured chart/diagram, delivered out-of-band from text. |
57
+ | `memory_extracted` | `signals: MemorySignal[]` | Only if your tenant has `extract_memory` enabled. Persist and replay via `memory`. |
58
+ | `done` | `summary: {...}` | Terminal success event. |
59
+ | `error` | `message`, `code` | Terminal failure event; stream ends. |
60
+
61
+ > **Forward compatibility.** The API only ever *adds* fields and event types
62
+ > within a contract version — it never renames, removes, or retypes existing
63
+ > ones. Always include a `default` branch in your `switch` and ignore unknown
64
+ > event types. This client surfaces them as `UnknownEvent` rather than throwing.
65
+
66
+ ## Request fields
67
+
68
+ | Field | Required | Description |
69
+ |------------------------|----------|-------------|
70
+ | `question` | ✅ | The end-user's question. |
71
+ | `request_id` | — | Client UUID for idempotent billing. Strongly recommended. |
72
+ | `demographics` | — | Small structured profile (`≤ 12 KB` JSON). |
73
+ | `health_context` | — | Free-form clinical context (vitals, EHR summary, wearable rollups). |
74
+ | `conversation_history` | — | Last turns only (max 8), `role` ∈ `user`/`assistant`. |
75
+ | `memory` | — | Raw memory signals to replay. |
76
+ | `tenant_id` | — | Optional hint; must match the key's tenant. |
77
+
78
+ ## Errors
79
+
80
+ Non-2xx HTTP responses throw `YesilApiError` with `status`, `code`
81
+ (server `error_code`), and `message`:
82
+
83
+ ```ts
84
+ import { YesilApiError } from '@yesilsci/health-ai-api';
85
+
86
+ try {
87
+ await client.chat({ question: '...' });
88
+ } catch (e) {
89
+ if (e instanceof YesilApiError) {
90
+ if (e.code === 'RATE_LIMITED') { /* back off */ }
91
+ if (e.status === 402) { /* quota exhausted / billing */ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ Common codes: `RATE_LIMITED` (429), quota/billing (402), `INVALID_API_KEY`
97
+ (401). In-stream failures arrive as an `error` event (see table above), not an
98
+ exception, unless you use the `chat()` convenience wrapper which re-throws them.
99
+
100
+ ## Account endpoints
101
+
102
+ ```ts
103
+ await client.billing(); // billing status
104
+ await client.quota(); // quota / rate-limit status
105
+ await client.usage(); // usage report
106
+ ```
107
+
108
+ ## Cancellation
109
+
110
+ Pass an `AbortSignal` to stop a stream early:
111
+
112
+ ```ts
113
+ const ac = new AbortController();
114
+ setTimeout(() => ac.abort(), 5_000);
115
+ for await (const ev of client.chatStream({ question: '...' }, { signal: ac.signal })) { /* … */ }
116
+ ```
117
+
118
+ ## Versioning
119
+
120
+ The client targets contract **v1** (`/api/v1`). When Yesil Health cuts a new
121
+ major version it ships alongside v1; bump `apiVersion` in `ClientOptions` to
122
+ migrate on your own schedule. v1 is never force-killed.
@@ -0,0 +1,61 @@
1
+ import type { BillingStatus, ChatEvent, ChatRequest, QuotaStatus, UsageReport } from './types.js';
2
+ export interface ClientOptions {
3
+ /** Base URL of the Yesil Health API, e.g. "https://api.yesilhealth.com". No trailing slash needed. */
4
+ baseUrl: string;
5
+ /** Your enterprise API key. Sent as the `X-API-Key` header. */
6
+ apiKey: string;
7
+ /** Contract version prefix. Defaults to "v1". Change only when migrating to a new major version. */
8
+ apiVersion?: string;
9
+ /** Optional custom fetch (for testing / non-standard runtimes). Defaults to global fetch. */
10
+ fetch?: typeof fetch;
11
+ }
12
+ /** Thrown for non-2xx HTTP responses. `code` mirrors the server's `error_code` when present. */
13
+ export declare class YesilApiError extends Error {
14
+ readonly status: number;
15
+ readonly code?: string;
16
+ readonly body?: unknown;
17
+ constructor(message: string, status: number, code?: string, body?: unknown);
18
+ }
19
+ export interface ChatStreamOptions {
20
+ /** Abort the stream early. */
21
+ signal?: AbortSignal;
22
+ }
23
+ /**
24
+ * Client for the Yesil Health enterprise (B2B) API.
25
+ *
26
+ * @example
27
+ * const client = new YesilHealthClient({ baseUrl: '...', apiKey: '...' });
28
+ * for await (const ev of client.chatStream({ question: 'Is metformin safe in CKD?' })) {
29
+ * if (ev.type === 'delta') process.stdout.write(ev.content);
30
+ * }
31
+ */
32
+ export declare class YesilHealthClient {
33
+ private readonly baseUrl;
34
+ private readonly apiKey;
35
+ private readonly prefix;
36
+ private readonly _fetch;
37
+ constructor(opts: ClientOptions);
38
+ /**
39
+ * Stream a chat answer as a sequence of typed events. The async generator
40
+ * yields every event verbatim; route on `event.type`. The stream ends after a
41
+ * `done` event (success) or an `error` event (failure).
42
+ */
43
+ chatStream(body: ChatRequest, opts?: ChatStreamOptions): AsyncGenerator<ChatEvent, void, unknown>;
44
+ /**
45
+ * Convenience wrapper: drains the stream, returns the full concatenated answer
46
+ * text plus the collected events. Use `chatStream` directly when you want to
47
+ * render tokens live.
48
+ */
49
+ chat(body: ChatRequest, opts?: ChatStreamOptions): Promise<{
50
+ text: string;
51
+ events: ChatEvent[];
52
+ }>;
53
+ /** Current billing status for the authenticated tenant. */
54
+ billing(): Promise<BillingStatus>;
55
+ /** Current quota / rate-limit status. */
56
+ quota(): Promise<QuotaStatus>;
57
+ /** Usage report for the authenticated tenant. */
58
+ usage(): Promise<UsageReport>;
59
+ private get;
60
+ private toError;
61
+ }
package/dist/client.js ADDED
@@ -0,0 +1,129 @@
1
+ import { parseSSE } from './sse.js';
2
+ /** Thrown for non-2xx HTTP responses. `code` mirrors the server's `error_code` when present. */
3
+ export class YesilApiError extends Error {
4
+ status;
5
+ code;
6
+ body;
7
+ constructor(message, status, code, body) {
8
+ super(message);
9
+ this.name = 'YesilApiError';
10
+ this.status = status;
11
+ this.code = code;
12
+ this.body = body;
13
+ }
14
+ }
15
+ /**
16
+ * Client for the Yesil Health enterprise (B2B) API.
17
+ *
18
+ * @example
19
+ * const client = new YesilHealthClient({ baseUrl: '...', apiKey: '...' });
20
+ * for await (const ev of client.chatStream({ question: 'Is metformin safe in CKD?' })) {
21
+ * if (ev.type === 'delta') process.stdout.write(ev.content);
22
+ * }
23
+ */
24
+ export class YesilHealthClient {
25
+ baseUrl;
26
+ apiKey;
27
+ prefix;
28
+ _fetch;
29
+ constructor(opts) {
30
+ if (!opts.baseUrl)
31
+ throw new Error('baseUrl is required');
32
+ if (!opts.apiKey)
33
+ throw new Error('apiKey is required');
34
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
35
+ this.apiKey = opts.apiKey;
36
+ this.prefix = `/api/${opts.apiVersion ?? 'v1'}`;
37
+ this._fetch = opts.fetch ?? globalThis.fetch;
38
+ if (!this._fetch) {
39
+ throw new Error('No fetch available. Use Node >=18 or pass a custom fetch in ClientOptions.');
40
+ }
41
+ }
42
+ /**
43
+ * Stream a chat answer as a sequence of typed events. The async generator
44
+ * yields every event verbatim; route on `event.type`. The stream ends after a
45
+ * `done` event (success) or an `error` event (failure).
46
+ */
47
+ async *chatStream(body, opts = {}) {
48
+ const res = await this._fetch(`${this.baseUrl}${this.prefix}/chat/stream`, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'X-API-Key': this.apiKey,
52
+ 'Content-Type': 'application/json',
53
+ Accept: 'text/event-stream',
54
+ },
55
+ body: JSON.stringify(body),
56
+ signal: opts.signal,
57
+ });
58
+ if (!res.ok || !res.body) {
59
+ throw await this.toError(res);
60
+ }
61
+ for await (const ev of parseSSE(res.body, opts.signal)) {
62
+ yield ev;
63
+ }
64
+ }
65
+ /**
66
+ * Convenience wrapper: drains the stream, returns the full concatenated answer
67
+ * text plus the collected events. Use `chatStream` directly when you want to
68
+ * render tokens live.
69
+ */
70
+ async chat(body, opts = {}) {
71
+ let text = '';
72
+ const events = [];
73
+ for await (const ev of this.chatStream(body, opts)) {
74
+ events.push(ev);
75
+ if (ev.type === 'delta')
76
+ text += ev.content;
77
+ if (ev.type === 'error') {
78
+ const err = ev;
79
+ throw new YesilApiError(err.message, 502, err.code, err);
80
+ }
81
+ }
82
+ return { text, events };
83
+ }
84
+ /** Current billing status for the authenticated tenant. */
85
+ billing() {
86
+ return this.get('/billing');
87
+ }
88
+ /** Current quota / rate-limit status. */
89
+ quota() {
90
+ return this.get('/quota');
91
+ }
92
+ /** Usage report for the authenticated tenant. */
93
+ usage() {
94
+ return this.get('/usage');
95
+ }
96
+ async get(path) {
97
+ const res = await this._fetch(`${this.baseUrl}${this.prefix}${path}`, {
98
+ method: 'GET',
99
+ headers: { 'X-API-Key': this.apiKey, Accept: 'application/json' },
100
+ });
101
+ if (!res.ok)
102
+ throw await this.toError(res);
103
+ return (await res.json());
104
+ }
105
+ async toError(res) {
106
+ let body;
107
+ let code;
108
+ let message = `HTTP ${res.status}`;
109
+ try {
110
+ body = await res.json();
111
+ const detail = body?.detail ?? body;
112
+ if (detail && typeof detail === 'object') {
113
+ code = detail.error_code;
114
+ const m = detail.message;
115
+ if (m)
116
+ message = m;
117
+ }
118
+ }
119
+ catch {
120
+ try {
121
+ message = (await res.text()) || message;
122
+ }
123
+ catch {
124
+ /* ignore */
125
+ }
126
+ }
127
+ return new YesilApiError(message, res.status, code, body);
128
+ }
129
+ }
@@ -0,0 +1,4 @@
1
+ export { YesilHealthClient, YesilApiError } from './client.js';
2
+ export type { ClientOptions, ChatStreamOptions } from './client.js';
3
+ export { parseSSE } from './sse.js';
4
+ export type { ChatRequest, ChatEvent, ConversationTurn, MemorySignal, MetaEvent, DeltaEvent, CitationEvent, GraphEvent, MemoryExtractedEvent, DoneEvent, ErrorEvent, UnknownEvent, BillingStatus, QuotaStatus, UsageReport, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { YesilHealthClient, YesilApiError } from './client.js';
2
+ export { parseSSE } from './sse.js';
package/dist/sse.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minimal SSE parser for the Yesil Health stream.
3
+ *
4
+ * The server emits each event as:
5
+ * event: <type>\n
6
+ * data: {"type":"<type>", ...}\n
7
+ * \n
8
+ *
9
+ * The `type` is carried both in the `event:` line and in the JSON body. We route
10
+ * on the JSON body's `type` (authoritative) and fall back to the `event:` line.
11
+ * Events whose data line isn't valid JSON are skipped defensively rather than
12
+ * crashing the stream.
13
+ */
14
+ /** Parse a raw fetch Response body (ReadableStream) into a stream of parsed JSON event objects. */
15
+ export declare function parseSSE(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Record<string, unknown>, void, unknown>;
package/dist/sse.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Minimal SSE parser for the Yesil Health stream.
3
+ *
4
+ * The server emits each event as:
5
+ * event: <type>\n
6
+ * data: {"type":"<type>", ...}\n
7
+ * \n
8
+ *
9
+ * The `type` is carried both in the `event:` line and in the JSON body. We route
10
+ * on the JSON body's `type` (authoritative) and fall back to the `event:` line.
11
+ * Events whose data line isn't valid JSON are skipped defensively rather than
12
+ * crashing the stream.
13
+ */
14
+ /** Parse a raw fetch Response body (ReadableStream) into a stream of parsed JSON event objects. */
15
+ export async function* parseSSE(body, signal) {
16
+ const reader = body.getReader();
17
+ const decoder = new TextDecoder();
18
+ let buffer = '';
19
+ try {
20
+ while (true) {
21
+ if (signal?.aborted)
22
+ throw new DOMException('Aborted', 'AbortError');
23
+ const { done, value } = await reader.read();
24
+ if (done)
25
+ break;
26
+ buffer += decoder.decode(value, { stream: true });
27
+ // Events are separated by a blank line. Drain every complete one.
28
+ let sep;
29
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
30
+ const rawEvent = buffer.slice(0, sep);
31
+ buffer = buffer.slice(sep + 2);
32
+ const parsed = parseEventBlock(rawEvent);
33
+ if (parsed)
34
+ yield parsed;
35
+ }
36
+ }
37
+ // Flush any trailing event without a final blank line.
38
+ const tail = parseEventBlock(buffer);
39
+ if (tail)
40
+ yield tail;
41
+ }
42
+ finally {
43
+ reader.releaseLock();
44
+ }
45
+ }
46
+ function parseEventBlock(block) {
47
+ let eventType;
48
+ const dataLines = [];
49
+ for (const line of block.split('\n')) {
50
+ if (line.startsWith('event:')) {
51
+ eventType = line.slice(6).trim();
52
+ }
53
+ else if (line.startsWith('data:')) {
54
+ dataLines.push(line.slice(5).trim());
55
+ }
56
+ }
57
+ if (dataLines.length === 0)
58
+ return null;
59
+ try {
60
+ const obj = JSON.parse(dataLines.join('\n'));
61
+ // Body `type` is authoritative; fall back to the SSE event: line.
62
+ if (typeof obj.type !== 'string' && eventType)
63
+ obj.type = eventType;
64
+ return obj;
65
+ }
66
+ catch {
67
+ return null; // unparseable data line — skip, don't kill the stream
68
+ }
69
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Wire types for the Yesil Health enterprise (B2B) API — contract v1.
3
+ *
4
+ * Contract rule (server side): ADD only. The server may add new fields or new
5
+ * SSE event types at any time; it will never rename, remove, or retype an
6
+ * existing one without cutting a new major version (/api/v2). This client is
7
+ * written to honor that: unknown event types surface as `UnknownEvent` instead
8
+ * of throwing, and extra fields on known events are preserved.
9
+ */
10
+ export interface ConversationTurn {
11
+ role: 'user' | 'assistant';
12
+ content: string;
13
+ }
14
+ /** Raw memory signal. Echoed back via the `memory_extracted` event so you can persist it. */
15
+ export interface MemorySignal {
16
+ domain?: string;
17
+ kind?: string;
18
+ flag?: string;
19
+ tag?: string;
20
+ signal_date?: string;
21
+ [key: string]: unknown;
22
+ }
23
+ export interface ChatRequest {
24
+ /** The end-user's question. Required, non-empty. */
25
+ question: string;
26
+ /** Small structured profile object. Large EHR/wearable summaries go in `health_context`. */
27
+ demographics?: Record<string, unknown>;
28
+ /** Raw memory signals. Max items enforced server-side. */
29
+ memory?: MemorySignal[];
30
+ /** Last few turns only (max 8). Role must be user|assistant. */
31
+ conversation_history?: ConversationTurn[];
32
+ /** Pre-built free-form health context block (vitals, EHR summary, wearable rollups, etc.). */
33
+ health_context?: string;
34
+ /** Client-generated UUID v1-5 for idempotent billing. Strongly recommended. */
35
+ request_id?: string;
36
+ /** Optional hint; must match the tenant resolved from the API key. */
37
+ tenant_id?: string;
38
+ }
39
+ /** Stream lifecycle / phase marker. Also carries mid-stream research previews. */
40
+ export interface MetaEvent {
41
+ type: 'meta';
42
+ phase?: string;
43
+ conversation_id?: string;
44
+ update_type?: string;
45
+ subtype?: string;
46
+ [key: string]: unknown;
47
+ }
48
+ /** A chunk of the answer text. Concatenate `content` across all deltas. */
49
+ export interface DeltaEvent {
50
+ type: 'delta';
51
+ content: string;
52
+ }
53
+ /** A literature citation surfaced for the current answer. */
54
+ export interface CitationEvent {
55
+ type: 'citation';
56
+ [key: string]: unknown;
57
+ }
58
+ /** A structured data graph (chart/diagram) payload, delivered out-of-band from text. */
59
+ export interface GraphEvent {
60
+ type: 'graph';
61
+ data?: Record<string, unknown>;
62
+ [key: string]: unknown;
63
+ }
64
+ /**
65
+ * Memory signals the AI extracted from this turn — only emitted if your tenant
66
+ * has the `extract_memory` feature flag enabled. Persist these on your side and
67
+ * pass them back in `memory` on future requests.
68
+ */
69
+ export interface MemoryExtractedEvent {
70
+ type: 'memory_extracted';
71
+ signals: MemorySignal[];
72
+ }
73
+ /** Terminal event. Carries the run summary (usage, research persist payload, etc.). */
74
+ export interface DoneEvent {
75
+ type: 'done';
76
+ summary?: Record<string, unknown>;
77
+ }
78
+ /** Stream-level error. The stream ends after this. */
79
+ export interface ErrorEvent {
80
+ type: 'error';
81
+ message: string;
82
+ code?: string;
83
+ }
84
+ /** Any event type this client version doesn't know about yet (forward-compatible). */
85
+ export interface UnknownEvent {
86
+ type: string;
87
+ [key: string]: unknown;
88
+ }
89
+ export type ChatEvent = MetaEvent | DeltaEvent | CitationEvent | GraphEvent | MemoryExtractedEvent | DoneEvent | ErrorEvent | UnknownEvent;
90
+ export interface BillingStatus {
91
+ [key: string]: unknown;
92
+ }
93
+ export interface QuotaStatus {
94
+ [key: string]: unknown;
95
+ }
96
+ export interface UsageReport {
97
+ [key: string]: unknown;
98
+ }
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Wire types for the Yesil Health enterprise (B2B) API — contract v1.
3
+ *
4
+ * Contract rule (server side): ADD only. The server may add new fields or new
5
+ * SSE event types at any time; it will never rename, remove, or retype an
6
+ * existing one without cutting a new major version (/api/v2). This client is
7
+ * written to honor that: unknown event types surface as `UnknownEvent` instead
8
+ * of throwing, and extra fields on known events are preserved.
9
+ */
10
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@yesilsci/health-ai-api",
3
+ "version": "0.1.0",
4
+ "description": "Official TypeScript client for the Yesil Health AI enterprise (B2B) API.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist", "README.md", "LICENSE"],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "typecheck": "tsc --noEmit -p tsconfig.json",
19
+ "example": "tsx examples/basic.ts"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": ["yesil", "health", "ai", "sse", "medical"],
25
+ "license": "MIT",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "devDependencies": {
30
+ "tsx": "^4.7.0",
31
+ "typescript": "^5.4.0"
32
+ }
33
+ }