@dudousxd/nestjs-agent-react 0.19.0 → 0.21.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/dist/media.cjs ADDED
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/media/index.ts
22
+ var media_exports = {};
23
+ __export(media_exports, {
24
+ MediaUploadError: () => MediaUploadError,
25
+ createMediaUpload: () => createMediaUpload,
26
+ mediaAttachments: () => mediaAttachments
27
+ });
28
+ module.exports = __toCommonJS(media_exports);
29
+ var import_nestjs_media_client = require("@dudousxd/nestjs-media-client");
30
+
31
+ // src/client.ts
32
+ function normalizeAgentPath(path) {
33
+ const trimmed = (path ?? "agent").replace(/^\/+|\/+$/g, "");
34
+ return trimmed === "" ? "" : `/${trimmed}`;
35
+ }
36
+ __name(normalizeAgentPath, "normalizeAgentPath");
37
+
38
+ // src/media/index.ts
39
+ function mediaAttachments(options = {}) {
40
+ return (file, uploadOptions, connection) => createMediaUpload({
41
+ ...options,
42
+ baseUrl: connection.baseUrl,
43
+ path: connection.path,
44
+ getHeaders: connection.headers,
45
+ fetch: connection.fetch,
46
+ ...connection.credentials !== void 0 ? {
47
+ credentials: connection.credentials
48
+ } : {}
49
+ })(file, uploadOptions);
50
+ }
51
+ __name(mediaAttachments, "mediaAttachments");
52
+ var MediaUploadError = class extends Error {
53
+ static {
54
+ __name(this, "MediaUploadError");
55
+ }
56
+ status;
57
+ method;
58
+ path;
59
+ constructor(status, method, path, statusText) {
60
+ super(`Attachment upload failed: ${method} ${path} \u2192 ${status} ${statusText}`), this.status = status, this.method = method, this.path = path;
61
+ this.name = "MediaUploadError";
62
+ }
63
+ };
64
+ function createMediaUpload(options = {}) {
65
+ const origin = (options.baseUrl ?? "").replace(/\/$/, "");
66
+ const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;
67
+ const baseFetch = /* @__PURE__ */ __name(() => options.fetch ?? fetch, "baseFetch");
68
+ const fetchWithCredentials = /* @__PURE__ */ __name((input, init) => baseFetch()(input, {
69
+ ...init,
70
+ ...options.credentials !== void 0 ? {
71
+ credentials: options.credentials
72
+ } : {}
73
+ }), "fetchWithCredentials");
74
+ const headers = /* @__PURE__ */ __name(async () => ({
75
+ ...options.headers,
76
+ ...await options.getHeaders?.() ?? {}
77
+ }), "headers");
78
+ const discard = /* @__PURE__ */ __name((mediaId) => {
79
+ void (async () => {
80
+ await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {
81
+ method: "DELETE",
82
+ headers: await headers()
83
+ });
84
+ })().catch(() => void 0);
85
+ }, "discard");
86
+ const call = /* @__PURE__ */ __name(async (method, url, body) => {
87
+ const response = await fetchWithCredentials(url, {
88
+ method,
89
+ headers: {
90
+ accept: "application/json",
91
+ ...body !== void 0 ? {
92
+ "content-type": "application/json"
93
+ } : {},
94
+ ...await headers()
95
+ },
96
+ ...body !== void 0 ? {
97
+ body: JSON.stringify(body)
98
+ } : {}
99
+ });
100
+ if (!response.ok) {
101
+ throw new MediaUploadError(response.status, method, url, response.statusText);
102
+ }
103
+ return await response.json();
104
+ }, "call");
105
+ return async (file, { signal, onProgress } = {}) => {
106
+ signal?.throwIfAborted();
107
+ const begun = await call("POST", base, {
108
+ filename: file.name,
109
+ contentType: file.type,
110
+ size: file.size
111
+ });
112
+ const onAbort = /* @__PURE__ */ __name(() => discard(begun.mediaId), "onAbort");
113
+ signal?.addEventListener("abort", onAbort, {
114
+ once: true
115
+ });
116
+ try {
117
+ const location = /^https?:\/\//.test(begun.location) ? begun.location : `${origin}${begun.location}`;
118
+ await (0, import_nestjs_media_client.streamChunks)(location, file, {
119
+ resume: false,
120
+ fetchImpl: fetchWithCredentials,
121
+ getHeaders: headers,
122
+ ...options.chunkSize !== void 0 ? {
123
+ chunkSize: options.chunkSize
124
+ } : {},
125
+ ...options.retries !== void 0 ? {
126
+ retries: options.retries
127
+ } : {},
128
+ ...signal !== void 0 ? {
129
+ signal
130
+ } : {},
131
+ onProgress: /* @__PURE__ */ __name((sent, total) => onProgress?.(total > 0 ? sent / total : 1), "onProgress")
132
+ });
133
+ signal?.throwIfAborted();
134
+ const attachment = await call("POST", `${base}/${encodeURIComponent(begun.mediaId)}/complete`);
135
+ onProgress?.(1);
136
+ return attachment;
137
+ } catch (error) {
138
+ if (!signal?.aborted) discard(begun.mediaId);
139
+ throw error;
140
+ } finally {
141
+ signal?.removeEventListener("abort", onAbort);
142
+ }
143
+ };
144
+ }
145
+ __name(createMediaUpload, "createMediaUpload");
146
+ // Annotate the CommonJS export names for ESM import in node:
147
+ 0 && (module.exports = {
148
+ MediaUploadError,
149
+ createMediaUpload,
150
+ mediaAttachments
151
+ });
152
+ //# sourceMappingURL=media.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/media/index.ts","../src/client.ts"],"sourcesContent":["import type { MessageAttachment } from '@dudousxd/nestjs-agent-core';\nimport { streamChunks } from '@dudousxd/nestjs-media-client';\nimport type { AttachmentUploadStrategy, UploadAttachmentOptions } from '../backend.js';\nimport { type AgentClientOptions, normalizeAgentPath } from '../client.js';\n\n/** Tuning for {@link mediaAttachments}; every field optional. */\nexport interface MediaAttachmentsOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB. */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/**\n * Resumable attachment uploads through `@dudousxd/nestjs-media`, in one line:\n *\n * ```tsx\n * <AgentProvider attachments={{ upload: mediaAttachments() }}>\n * ```\n *\n * Reuses the client's own connection (origin, path, headers, credentials), so it needs no\n * configuration. Also takes `new AgentClient({ attachments: { upload: mediaAttachments() } })`.\n */\nexport function mediaAttachments(options: MediaAttachmentsOptions = {}): AttachmentUploadStrategy {\n return (file, uploadOptions, connection) =>\n createMediaUpload({\n ...options,\n baseUrl: connection.baseUrl,\n path: connection.path,\n getHeaders: connection.headers,\n fetch: connection.fetch,\n ...(connection.credentials !== undefined ? { credentials: connection.credentials } : {}),\n })(file, uploadOptions);\n}\n\n/**\n * Where and how {@link createMediaUpload} talks to the server. The connection fields are\n * {@link AgentClientOptions}' — pass the same ones your `AgentClient` gets, so the tus requests\n * carry the same cookies / CSRF header / bearer token as the rest of the chat.\n */\nexport interface MediaUploadOptions extends AgentClientOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB (nestjs-media-client's default). */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/** What `useAttachments({ upload })` and `AgentBackend.uploadAttachment` both take. */\nexport type AttachmentUpload = (\n file: File,\n options?: UploadAttachmentOptions,\n) => Promise<MessageAttachment>;\n\n/**\n * A non-2xx answer from the agent's upload routes. Carries `status` like `AgentHttpError` does\n * (413 too large, 415 type refused, 404 not yours) — its own class because this subpath is bundled\n * apart from the root entry, where an `instanceof AgentHttpError` would not match a copy.\n */\nexport class MediaUploadError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n ) {\n super(`Attachment upload failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'MediaUploadError';\n }\n}\n\ninterface BeginResponse {\n mediaId: string;\n location: string;\n}\n\n/**\n * A resumable `upload` for `useAttachments`, backed by `@dudousxd/nestjs-media` on the server\n * (`AgentMediaAttachmentsModule` from `@dudousxd/nestjs-agent/media`):\n *\n * 1. `POST <path>/attachments/uploads` — the agent validates the file and opens a tus session the\n * actor owns;\n * 2. the bytes stream to nestjs-media's own tus endpoint in chunks (`streamChunks`), reporting\n * progress and honouring the abort signal;\n * 3. `POST <path>/attachments/uploads/:mediaId/complete` — the agent confirms the bytes landed and\n * answers the `MessageAttachment` the turn will reference by `mediaId`.\n *\n * An aborted or failed upload is discarded server-side (`DELETE`), best effort.\n */\nexport function createMediaUpload(options: MediaUploadOptions = {}): AttachmentUpload {\n const origin = (options.baseUrl ?? '').replace(/\\/$/, '');\n const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;\n const baseFetch = (): typeof fetch => options.fetch ?? fetch;\n // Every request — the agent's and media's tus PATCHes — rides the same credentials mode.\n const fetchWithCredentials: typeof fetch = (input, init) =>\n baseFetch()(input, {\n ...init,\n ...(options.credentials !== undefined ? { credentials: options.credentials } : {}),\n });\n const headers = async (): Promise<Record<string, string>> => ({\n ...options.headers,\n ...((await options.getHeaders?.()) ?? {}),\n });\n const discard = (mediaId: string): void => {\n void (async () => {\n await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {\n method: 'DELETE',\n headers: await headers(),\n });\n })().catch(() => undefined);\n };\n const call = async <T>(method: string, url: string, body?: unknown): Promise<T> => {\n const response = await fetchWithCredentials(url, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await headers()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n if (!response.ok) {\n throw new MediaUploadError(response.status, method, url, response.statusText);\n }\n return (await response.json()) as T;\n };\n\n return async (file, { signal, onProgress } = {}) => {\n signal?.throwIfAborted();\n const begun = await call<BeginResponse>('POST', base, {\n filename: file.name,\n contentType: file.type,\n size: file.size,\n });\n const onAbort = () => discard(begun.mediaId);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n const location = /^https?:\\/\\//.test(begun.location)\n ? begun.location\n : `${origin}${begun.location}`;\n await streamChunks(location, file, {\n resume: false,\n fetchImpl: fetchWithCredentials,\n getHeaders: headers,\n ...(options.chunkSize !== undefined ? { chunkSize: options.chunkSize } : {}),\n ...(options.retries !== undefined ? { retries: options.retries } : {}),\n ...(signal !== undefined ? { signal } : {}),\n onProgress: (sent, total) => onProgress?.(total > 0 ? sent / total : 1),\n });\n signal?.throwIfAborted();\n const attachment = await call<MessageAttachment>(\n 'POST',\n `${base}/${encodeURIComponent(begun.mediaId)}/complete`,\n );\n onProgress?.(1);\n return attachment;\n } catch (error) {\n if (!signal?.aborted) discard(begun.mediaId);\n throw error;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n","import type {\n AgentCatalogEntry,\n MessageAttachment,\n MessageFeedback,\n ModelCatalogView,\n QuotaReport,\n QuotaView,\n SkillCatalogEntry,\n ThreadDetail,\n ThreadSummary,\n ToolCatalogEntry,\n} from '@dudousxd/nestjs-agent-core';\nimport type {\n AgentBackend,\n AgentConnection,\n AttachmentUploadStrategy,\n ChatStreamRequest,\n ChatStreamResponse,\n MessageFeedbackInput,\n ResumeStreamRequest,\n ThreadPatch,\n UploadAttachmentOptions,\n} from './backend.js';\n\nexport type { ThreadPatch } from './backend.js';\n\n/**\n * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can\n * branch (e.g. 403 → \"not your thread\", 429 → quota) instead of string-matching a generic Error.\n */\nexport class AgentHttpError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n ) {\n super(`Agent request failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'AgentHttpError';\n }\n}\n\n/** The quota-today read-model: usage, the configured limit (null → unlimited), and USD spend. */\nexport type QuotaToday = QuotaView;\n\nexport interface CancelResult {\n aborted: boolean;\n}\n\nexport interface OkResult {\n ok: boolean;\n}\n\nexport interface AgentClientOptions {\n /**\n * The server's origin, e.g. `https://api.example.com`. Defaults to `''` (same origin). The\n * agent's route prefix is {@link AgentClientOptions.path}, not part of this.\n */\n baseUrl?: string;\n /**\n * The agent's route prefix — `AgentModule`'s `path`, with any global prefix in front\n * (`'api/agent'`). Leading/trailing slashes are optional. Defaults to `'agent'`.\n */\n path?: string;\n /** Static headers merged into every request. */\n headers?: Record<string, string>;\n /**\n * Resolved per request — for short-lived bearer tokens, or a CSRF header read from a cookie\n * (`{ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }`), which has to be read at request time because\n * the server may rotate it.\n */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /**\n * Forwarded to fetch so cookie auth works. Same-origin requests send cookies by default; set\n * `'include'` when the API lives on another origin (and have it answer with credentialed CORS).\n */\n credentials?: RequestCredentials;\n /** Injectable for tests / non-browser runtimes. */\n fetch?: typeof fetch;\n /** Attachment uploads. */\n attachments?: {\n /**\n * How `uploadAttachment` uploads. Omitted → `POST <path>/attachments` (multipart). Pass\n * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through\n * nestjs-media, or your own {@link AttachmentUploadStrategy}.\n */\n upload?: AttachmentUploadStrategy;\n };\n}\n\n/** `'/api/agent'` from `'api/agent'`, `'/api/agent/'`, …; `''` for an empty path. */\nexport function normalizeAgentPath(path: string | undefined): string {\n const trimmed = (path ?? 'agent').replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `/${trimmed}`;\n}\n\nconst HEADER_RUN_ID = 'x-agent-run-id';\nconst HEADER_THREAD_ID = 'x-agent-thread-id';\n\n/**\n * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.\n * Used by `useAgentChat`, but standalone-usable (vanilla fetch, no React).\n */\nexport class AgentClient implements AgentBackend {\n constructor(private readonly options: AgentClientOptions = {}) {}\n\n /** `POST <path>/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */\n async openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse> {\n const response = await this.fetchImpl()(`${this.root()}/chat`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n body: JSON.stringify(request.body),\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (!response.ok || !response.body) {\n throw new AgentHttpError(\n response.status,\n 'POST',\n `${this.agentPath()}/chat`,\n response.statusText,\n );\n }\n return streamResponse(response);\n }\n\n /**\n * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is\n * streaming under that id (404).\n */\n async resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null> {\n const path = `/chat/${encodeURIComponent(request.runId)}/stream`;\n const query = request.after !== undefined && request.after > 0 ? `?after=${request.after}` : '';\n const response = await this.fetchImpl()(`${this.root()}${path}${query}`, {\n method: 'GET',\n headers: {\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 404) {\n return null;\n }\n if (!response.ok || !response.body) {\n throw new AgentHttpError(\n response.status,\n 'GET',\n `${this.agentPath()}${path}`,\n response.statusText,\n );\n }\n return streamResponse(response);\n }\n\n /**\n * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers\n * the stored rating.\n */\n setMessageFeedback(\n messageId: string,\n input: MessageFeedbackInput,\n ): Promise<{ feedback: MessageFeedback | null }> {\n return this.request<{ feedback: MessageFeedback | null }>(\n 'POST',\n `/messages/${encodeURIComponent(messageId)}/feedback`,\n input,\n );\n }\n\n listThreads(): Promise<ThreadSummary[]> {\n return this.request<ThreadSummary[]>('GET', '/threads');\n }\n\n /**\n * The skills this caller can invoke right now, scope-resolved — the same list, built by the same\n * call, that the model is offered, so what a user can type after a `/` and what the agent can\n * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to\n * one conversation; omitted, the server reads it as a brand-new thread.\n */\n listSkills(threadId?: string): Promise<SkillCatalogEntry[]> {\n const query = threadId === undefined ? '' : `?threadId=${encodeURIComponent(threadId)}`;\n return this.request<SkillCatalogEntry[]>('GET', `/skills${query}`);\n }\n\n /**\n * The tools this caller can reach through `agent` (the default agent when omitted), each with the\n * server-declared `presentation` a chat narrates it by — the same list the model is offered.\n * Prefer {@link useToolCatalog}, which fetches it once and shares it.\n */\n listTools(agent?: string): Promise<ToolCatalogEntry[]> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ToolCatalogEntry[]>('GET', `/tools${query}`);\n }\n\n getThread(id: string): Promise<ThreadDetail> {\n return this.request<ThreadDetail>('GET', `/threads/${encodeURIComponent(id)}`);\n }\n\n deleteThread(id: string): Promise<void> {\n return this.request<void>('DELETE', `/threads/${encodeURIComponent(id)}`);\n }\n\n forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary> {\n return this.request<ThreadSummary>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`,\n );\n }\n\n renameThread(id: string, title: string): Promise<OkResult> {\n return this.updateThread(id, { title });\n }\n\n /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */\n updateThread(id: string, patch: ThreadPatch): Promise<OkResult> {\n return this.request<OkResult>('PATCH', `/threads/${encodeURIComponent(id)}`, patch);\n }\n\n /**\n * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —\n * mirrors the backend's `POST <path>/attachments`. The returned {@link MessageAttachment} is\n * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.\n */\n async uploadAttachment(\n file: File,\n options: UploadAttachmentOptions = {},\n ): Promise<MessageAttachment> {\n const upload = this.options.attachments?.upload;\n if (upload !== undefined) {\n return upload(file, options, this.connection());\n }\n // `fetch` cannot observe an upload's progress; XHR can. Only when someone is listening, and\n // never when a `fetch` was injected (tests, non-browser runtimes).\n if (\n options.onProgress !== undefined &&\n this.options.fetch === undefined &&\n typeof XMLHttpRequest !== 'undefined'\n ) {\n return this.uploadWithProgress(file, options);\n }\n const formData = new FormData();\n formData.append('file', file);\n const response = await this.fetchImpl()(`${this.root()}/attachments`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n },\n body: formData,\n ...this.credentials(),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n });\n const attachment = await this.handleResponse<MessageAttachment>(\n response,\n 'POST',\n `${this.agentPath()}/attachments`,\n );\n options.onProgress?.(1);\n return attachment;\n }\n\n private async uploadWithProgress(\n file: File,\n { signal, onProgress }: UploadAttachmentOptions,\n ): Promise<MessageAttachment> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n };\n const url = `${this.root()}/attachments`;\n return new Promise<MessageAttachment>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', url);\n for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value);\n // Same-origin requests carry cookies regardless; this is the cross-origin opt-in.\n xhr.withCredentials = this.options.credentials === 'include';\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable && event.total > 0) onProgress?.(event.loaded / event.total);\n };\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(\n new AgentHttpError(\n xhr.status,\n 'POST',\n `${this.agentPath()}/attachments`,\n xhr.statusText,\n ),\n );\n return;\n }\n onProgress?.(1);\n resolve(JSON.parse(xhr.responseText) as MessageAttachment);\n };\n xhr.onerror = () => reject(new TypeError('Network error while uploading the attachment'));\n xhr.onabort = () => reject(new DOMException('The upload was aborted', 'AbortError'));\n if (signal !== undefined) {\n if (signal.aborted) {\n reject(new DOMException('The upload was aborted', 'AbortError'));\n return;\n }\n signal.addEventListener('abort', () => xhr.abort(), { once: true });\n }\n const formData = new FormData();\n formData.append('file', file);\n xhr.send(formData);\n });\n }\n\n promoteThread(id: string): Promise<OkResult> {\n return this.request<OkResult>('POST', `/threads/${encodeURIComponent(id)}/promote`);\n }\n\n truncateFromMessage(threadId: string, messageId: string): Promise<OkResult> {\n return this.request<OkResult>(\n 'DELETE',\n `/threads/${encodeURIComponent(threadId)}/from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** `GET <path>/models?agent=` — the models this caller may pick, grouped by provider. */\n listModels(agent?: string): Promise<ModelCatalogView> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ModelCatalogView>('GET', `/models${query}`);\n }\n\n /** `GET <path>/agents` — the registered agents, the default one flagged. */\n listAgents(): Promise<AgentCatalogEntry[]> {\n return this.request<AgentCatalogEntry[]>('GET', '/agents');\n }\n\n /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */\n getQuota(): Promise<QuotaReport> {\n return this.request<QuotaReport>('GET', '/quota');\n }\n\n getQuotaToday(): Promise<QuotaToday> {\n return this.request<QuotaToday>('GET', '/quota/today');\n }\n\n cancelStream(runId: string): Promise<CancelResult> {\n return this.request<CancelResult>('POST', `/chat/${encodeURIComponent(runId)}/cancel`);\n }\n\n /**\n * `remember` approves later calls of the same tool in the same thread; `via` names the surface\n * the decision came through (the server records `'web'` when omitted).\n */\n approveToolCall(input: { toolCallId: string; remember?: boolean; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/approve', input);\n }\n\n rejectToolCall(input: { toolCallId: string; reason?: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/reject', input);\n }\n\n /**\n * Settle a parked question set. `answers` is questionId → chosen option values; a question left\n * out takes the pre-picked default the request carried, resolved server-side against the request\n * the run already holds. Omit the whole object and the user has confirmed every pre-picked\n * answer — which is the point of the surface, so it is a valid submission rather than a blank.\n */\n answerToolCall(input: {\n toolCallId: string;\n answers?: Record<string, string[]>;\n }): Promise<void> {\n return this.request<void>('POST', '/tool-call/answer', input);\n }\n\n /**\n * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same\n * values a confirmation would, and persists differently on purpose — only one of them is\n * evidence the user chose them.\n */\n skipToolCall(input: { toolCallId: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/skip', input);\n }\n\n private fetchImpl(): typeof fetch {\n return this.options.fetch ?? globalThis.fetch;\n }\n\n /** This client's connection, for an {@link AttachmentUploadStrategy}. */\n private connection(): AgentConnection {\n return {\n baseUrl: this.baseUrl(),\n path: this.agentPath(),\n headers: () => this.resolveHeaders(),\n fetch: this.fetchImpl(),\n ...this.credentials(),\n };\n }\n\n private baseUrl(): string {\n return (this.options.baseUrl ?? '').replace(/\\/+$/, '');\n }\n\n private agentPath(): string {\n return normalizeAgentPath(this.options.path);\n }\n\n /** Origin + agent path: what every route hangs off. */\n private root(): string {\n return `${this.baseUrl()}${this.agentPath()}`;\n }\n\n private async resolveHeaders(): Promise<Record<string, string>> {\n const dynamic = (await this.options.getHeaders?.()) ?? {};\n return { ...this.options.headers, ...dynamic };\n }\n\n private credentials(): { credentials?: RequestCredentials } {\n return this.options.credentials !== undefined ? { credentials: this.options.credentials } : {};\n }\n\n private async request<T>(method: string, route: string, body?: unknown): Promise<T> {\n const path = `${this.agentPath()}${route}`;\n const response = await this.fetchImpl()(`${this.baseUrl()}${path}`, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await this.resolveHeaders()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n ...this.credentials(),\n });\n return this.handleResponse<T>(response, method, path);\n }\n\n private async handleResponse<T>(response: Response, method: string, path: string): Promise<T> {\n if (!response.ok) {\n throw new AgentHttpError(response.status, method, path, response.statusText);\n }\n if (response.status === 204) return undefined as T;\n const text = await response.text();\n if (!text) return undefined as T;\n return JSON.parse(text) as T;\n }\n}\n\nfunction streamResponse(response: Response): ChatStreamResponse {\n const body = response.body as ReadableStream<Uint8Array>;\n const runId = response.headers?.get(HEADER_RUN_ID) ?? undefined;\n const threadId = response.headers?.get(HEADER_THREAD_ID) ?? undefined;\n return {\n body,\n ...(runId ? { runId } : {}),\n ...(threadId ? { threadId } : {}),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AACA;;;;;;;iCAA6B;;;AC0FtB,SAASA,mBAAmBC,MAAwB;AACzD,QAAMC,WAAWD,QAAQ,SAASE,QAAQ,cAAc,EAAA;AACxD,SAAOD,YAAY,KAAK,KAAK,IAAIA,OAAAA;AACnC;AAHgBF;;;ADpET,SAASI,iBAAiBC,UAAmC,CAAC,GAAC;AACpE,SAAO,CAACC,MAAMC,eAAeC,eAC3BC,kBAAkB;IAChB,GAAGJ;IACHK,SAASF,WAAWE;IACpBC,MAAMH,WAAWG;IACjBC,YAAYJ,WAAWK;IACvBC,OAAON,WAAWM;IAClB,GAAIN,WAAWO,gBAAgBC,SAAY;MAAED,aAAaP,WAAWO;IAAY,IAAI,CAAC;EACxF,CAAA,EAAGT,MAAMC,aAAAA;AACb;AAVgBH;AAmCT,IAAMa,mBAAN,cAA+BC,MAAAA;EAzDtC,OAyDsCA;;;;;;EACpC,YACWC,QACAC,QACAT,MACTU,YACA;AACA,UAAM,6BAA6BD,MAAAA,IAAUT,IAAAA,WAAUQ,MAAAA,IAAUE,UAAAA,EAAY,GAAA,KALpEF,SAAAA,QAAAA,KACAC,SAAAA,QAAAA,KACAT,OAAAA;AAIT,SAAKW,OAAO;EACd;AACF;AAoBO,SAASb,kBAAkBJ,UAA8B,CAAC,GAAC;AAChE,QAAMkB,UAAUlB,QAAQK,WAAW,IAAIc,QAAQ,OAAO,EAAA;AACtD,QAAMC,OAAO,GAAGF,MAAAA,GAASG,mBAAmBrB,QAAQM,IAAI,CAAA;AACxD,QAAMgB,YAAY,6BAAoBtB,QAAQS,SAASA,OAArC;AAElB,QAAMc,uBAAqC,wBAACC,OAAOC,SACjDH,UAAAA,EAAYE,OAAO;IACjB,GAAGC;IACH,GAAIzB,QAAQU,gBAAgBC,SAAY;MAAED,aAAaV,QAAQU;IAAY,IAAI,CAAC;EAClF,CAAA,GAJyC;AAK3C,QAAMF,UAAU,oCAA8C;IAC5D,GAAGR,QAAQQ;IACX,GAAK,MAAMR,QAAQO,aAAU,KAAS,CAAC;EACzC,IAHgB;AAIhB,QAAMmB,UAAU,wBAACC,YAAAA;AACf,UAAM,YAAA;AACJ,YAAMJ,qBAAqB,GAAGH,IAAAA,IAAQQ,mBAAmBD,OAAAA,CAAAA,IAAY;QACnEZ,QAAQ;QACRP,SAAS,MAAMA,QAAAA;MACjB,CAAA;IACF,GAAA,EAAKqB,MAAM,MAAMlB,MAAAA;EACnB,GAPgB;AAQhB,QAAMmB,OAAO,8BAAUf,QAAgBgB,KAAaC,SAAAA;AAClD,UAAMC,WAAW,MAAMV,qBAAqBQ,KAAK;MAC/ChB;MACAP,SAAS;QACP0B,QAAQ;QACR,GAAIF,SAASrB,SAAY;UAAE,gBAAgB;QAAmB,IAAI,CAAC;QACnE,GAAI,MAAMH,QAAAA;MACZ;MACA,GAAIwB,SAASrB,SAAY;QAAEqB,MAAMG,KAAKC,UAAUJ,IAAAA;MAAM,IAAI,CAAC;IAC7D,CAAA;AACA,QAAI,CAACC,SAASI,IAAI;AAChB,YAAM,IAAIzB,iBAAiBqB,SAASnB,QAAQC,QAAQgB,KAAKE,SAASjB,UAAU;IAC9E;AACA,WAAQ,MAAMiB,SAASK,KAAI;EAC7B,GAda;AAgBb,SAAO,OAAOrC,MAAM,EAAEsC,QAAQC,WAAU,IAAK,CAAC,MAAC;AAC7CD,YAAQE,eAAAA;AACR,UAAMC,QAAQ,MAAMZ,KAAoB,QAAQV,MAAM;MACpDuB,UAAU1C,KAAKgB;MACf2B,aAAa3C,KAAK4C;MAClBC,MAAM7C,KAAK6C;IACb,CAAA;AACA,UAAMC,UAAU,6BAAMrB,QAAQgB,MAAMf,OAAO,GAA3B;AAChBY,YAAQS,iBAAiB,SAASD,SAAS;MAAEE,MAAM;IAAK,CAAA;AACxD,QAAI;AACF,YAAMC,WAAW,eAAeC,KAAKT,MAAMQ,QAAQ,IAC/CR,MAAMQ,WACN,GAAGhC,MAAAA,GAASwB,MAAMQ,QAAQ;AAC9B,gBAAME,yCAAaF,UAAUjD,MAAM;QACjCoD,QAAQ;QACRC,WAAW/B;QACXhB,YAAYC;QACZ,GAAIR,QAAQuD,cAAc5C,SAAY;UAAE4C,WAAWvD,QAAQuD;QAAU,IAAI,CAAC;QAC1E,GAAIvD,QAAQwD,YAAY7C,SAAY;UAAE6C,SAASxD,QAAQwD;QAAQ,IAAI,CAAC;QACpE,GAAIjB,WAAW5B,SAAY;UAAE4B;QAAO,IAAI,CAAC;QACzCC,YAAY,wBAACiB,MAAMC,UAAUlB,aAAakB,QAAQ,IAAID,OAAOC,QAAQ,CAAA,GAAzD;MACd,CAAA;AACAnB,cAAQE,eAAAA;AACR,YAAMkB,aAAa,MAAM7B,KACvB,QACA,GAAGV,IAAAA,IAAQQ,mBAAmBc,MAAMf,OAAO,CAAA,WAAY;AAEzDa,mBAAa,CAAA;AACb,aAAOmB;IACT,SAASC,OAAO;AACd,UAAI,CAACrB,QAAQsB,QAASnC,SAAQgB,MAAMf,OAAO;AAC3C,YAAMiC;IACR,UAAA;AACErB,cAAQuB,oBAAoB,SAASf,OAAAA;IACvC;EACF;AACF;AA1EgB3C;","names":["normalizeAgentPath","path","trimmed","replace","mediaAttachments","options","file","uploadOptions","connection","createMediaUpload","baseUrl","path","getHeaders","headers","fetch","credentials","undefined","MediaUploadError","Error","status","method","statusText","name","origin","replace","base","normalizeAgentPath","baseFetch","fetchWithCredentials","input","init","discard","mediaId","encodeURIComponent","catch","call","url","body","response","accept","JSON","stringify","ok","json","signal","onProgress","throwIfAborted","begun","filename","contentType","type","size","onAbort","addEventListener","once","location","test","streamChunks","resume","fetchImpl","chunkSize","retries","sent","total","attachment","error","aborted","removeEventListener"]}
@@ -0,0 +1,61 @@
1
+ import { MessageAttachment } from '@dudousxd/nestjs-agent-core';
2
+ import { U as UploadAttachmentOptions, d as AgentClientOptions, a as AttachmentUploadStrategy } from './client-ePlgRwP-.cjs';
3
+
4
+ /** Tuning for {@link mediaAttachments}; every field optional. */
5
+ interface MediaAttachmentsOptions {
6
+ /** Bytes per tus `PATCH`. Default 5 MiB. */
7
+ chunkSize?: number;
8
+ /** Attempts per chunk before the upload fails. Default 3. */
9
+ retries?: number;
10
+ }
11
+ /**
12
+ * Resumable attachment uploads through `@dudousxd/nestjs-media`, in one line:
13
+ *
14
+ * ```tsx
15
+ * <AgentProvider attachments={{ upload: mediaAttachments() }}>
16
+ * ```
17
+ *
18
+ * Reuses the client's own connection (origin, path, headers, credentials), so it needs no
19
+ * configuration. Also takes `new AgentClient({ attachments: { upload: mediaAttachments() } })`.
20
+ */
21
+ declare function mediaAttachments(options?: MediaAttachmentsOptions): AttachmentUploadStrategy;
22
+ /**
23
+ * Where and how {@link createMediaUpload} talks to the server. The connection fields are
24
+ * {@link AgentClientOptions}' — pass the same ones your `AgentClient` gets, so the tus requests
25
+ * carry the same cookies / CSRF header / bearer token as the rest of the chat.
26
+ */
27
+ interface MediaUploadOptions extends AgentClientOptions {
28
+ /** Bytes per tus `PATCH`. Default 5 MiB (nestjs-media-client's default). */
29
+ chunkSize?: number;
30
+ /** Attempts per chunk before the upload fails. Default 3. */
31
+ retries?: number;
32
+ }
33
+ /** What `useAttachments({ upload })` and `AgentBackend.uploadAttachment` both take. */
34
+ type AttachmentUpload = (file: File, options?: UploadAttachmentOptions) => Promise<MessageAttachment>;
35
+ /**
36
+ * A non-2xx answer from the agent's upload routes. Carries `status` like `AgentHttpError` does
37
+ * (413 too large, 415 type refused, 404 not yours) — its own class because this subpath is bundled
38
+ * apart from the root entry, where an `instanceof AgentHttpError` would not match a copy.
39
+ */
40
+ declare class MediaUploadError extends Error {
41
+ readonly status: number;
42
+ readonly method: string;
43
+ readonly path: string;
44
+ constructor(status: number, method: string, path: string, statusText: string);
45
+ }
46
+ /**
47
+ * A resumable `upload` for `useAttachments`, backed by `@dudousxd/nestjs-media` on the server
48
+ * (`AgentMediaAttachmentsModule` from `@dudousxd/nestjs-agent/media`):
49
+ *
50
+ * 1. `POST <path>/attachments/uploads` — the agent validates the file and opens a tus session the
51
+ * actor owns;
52
+ * 2. the bytes stream to nestjs-media's own tus endpoint in chunks (`streamChunks`), reporting
53
+ * progress and honouring the abort signal;
54
+ * 3. `POST <path>/attachments/uploads/:mediaId/complete` — the agent confirms the bytes landed and
55
+ * answers the `MessageAttachment` the turn will reference by `mediaId`.
56
+ *
57
+ * An aborted or failed upload is discarded server-side (`DELETE`), best effort.
58
+ */
59
+ declare function createMediaUpload(options?: MediaUploadOptions): AttachmentUpload;
60
+
61
+ export { type AttachmentUpload, type MediaAttachmentsOptions, MediaUploadError, type MediaUploadOptions, createMediaUpload, mediaAttachments };
@@ -0,0 +1,61 @@
1
+ import { MessageAttachment } from '@dudousxd/nestjs-agent-core';
2
+ import { U as UploadAttachmentOptions, d as AgentClientOptions, a as AttachmentUploadStrategy } from './client-ePlgRwP-.js';
3
+
4
+ /** Tuning for {@link mediaAttachments}; every field optional. */
5
+ interface MediaAttachmentsOptions {
6
+ /** Bytes per tus `PATCH`. Default 5 MiB. */
7
+ chunkSize?: number;
8
+ /** Attempts per chunk before the upload fails. Default 3. */
9
+ retries?: number;
10
+ }
11
+ /**
12
+ * Resumable attachment uploads through `@dudousxd/nestjs-media`, in one line:
13
+ *
14
+ * ```tsx
15
+ * <AgentProvider attachments={{ upload: mediaAttachments() }}>
16
+ * ```
17
+ *
18
+ * Reuses the client's own connection (origin, path, headers, credentials), so it needs no
19
+ * configuration. Also takes `new AgentClient({ attachments: { upload: mediaAttachments() } })`.
20
+ */
21
+ declare function mediaAttachments(options?: MediaAttachmentsOptions): AttachmentUploadStrategy;
22
+ /**
23
+ * Where and how {@link createMediaUpload} talks to the server. The connection fields are
24
+ * {@link AgentClientOptions}' — pass the same ones your `AgentClient` gets, so the tus requests
25
+ * carry the same cookies / CSRF header / bearer token as the rest of the chat.
26
+ */
27
+ interface MediaUploadOptions extends AgentClientOptions {
28
+ /** Bytes per tus `PATCH`. Default 5 MiB (nestjs-media-client's default). */
29
+ chunkSize?: number;
30
+ /** Attempts per chunk before the upload fails. Default 3. */
31
+ retries?: number;
32
+ }
33
+ /** What `useAttachments({ upload })` and `AgentBackend.uploadAttachment` both take. */
34
+ type AttachmentUpload = (file: File, options?: UploadAttachmentOptions) => Promise<MessageAttachment>;
35
+ /**
36
+ * A non-2xx answer from the agent's upload routes. Carries `status` like `AgentHttpError` does
37
+ * (413 too large, 415 type refused, 404 not yours) — its own class because this subpath is bundled
38
+ * apart from the root entry, where an `instanceof AgentHttpError` would not match a copy.
39
+ */
40
+ declare class MediaUploadError extends Error {
41
+ readonly status: number;
42
+ readonly method: string;
43
+ readonly path: string;
44
+ constructor(status: number, method: string, path: string, statusText: string);
45
+ }
46
+ /**
47
+ * A resumable `upload` for `useAttachments`, backed by `@dudousxd/nestjs-media` on the server
48
+ * (`AgentMediaAttachmentsModule` from `@dudousxd/nestjs-agent/media`):
49
+ *
50
+ * 1. `POST <path>/attachments/uploads` — the agent validates the file and opens a tus session the
51
+ * actor owns;
52
+ * 2. the bytes stream to nestjs-media's own tus endpoint in chunks (`streamChunks`), reporting
53
+ * progress and honouring the abort signal;
54
+ * 3. `POST <path>/attachments/uploads/:mediaId/complete` — the agent confirms the bytes landed and
55
+ * answers the `MessageAttachment` the turn will reference by `mediaId`.
56
+ *
57
+ * An aborted or failed upload is discarded server-side (`DELETE`), best effort.
58
+ */
59
+ declare function createMediaUpload(options?: MediaUploadOptions): AttachmentUpload;
60
+
61
+ export { type AttachmentUpload, type MediaAttachmentsOptions, MediaUploadError, type MediaUploadOptions, createMediaUpload, mediaAttachments };
package/dist/media.js ADDED
@@ -0,0 +1,127 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/media/index.ts
5
+ import { streamChunks } from "@dudousxd/nestjs-media-client";
6
+
7
+ // src/client.ts
8
+ function normalizeAgentPath(path) {
9
+ const trimmed = (path ?? "agent").replace(/^\/+|\/+$/g, "");
10
+ return trimmed === "" ? "" : `/${trimmed}`;
11
+ }
12
+ __name(normalizeAgentPath, "normalizeAgentPath");
13
+
14
+ // src/media/index.ts
15
+ function mediaAttachments(options = {}) {
16
+ return (file, uploadOptions, connection) => createMediaUpload({
17
+ ...options,
18
+ baseUrl: connection.baseUrl,
19
+ path: connection.path,
20
+ getHeaders: connection.headers,
21
+ fetch: connection.fetch,
22
+ ...connection.credentials !== void 0 ? {
23
+ credentials: connection.credentials
24
+ } : {}
25
+ })(file, uploadOptions);
26
+ }
27
+ __name(mediaAttachments, "mediaAttachments");
28
+ var MediaUploadError = class extends Error {
29
+ static {
30
+ __name(this, "MediaUploadError");
31
+ }
32
+ status;
33
+ method;
34
+ path;
35
+ constructor(status, method, path, statusText) {
36
+ super(`Attachment upload failed: ${method} ${path} \u2192 ${status} ${statusText}`), this.status = status, this.method = method, this.path = path;
37
+ this.name = "MediaUploadError";
38
+ }
39
+ };
40
+ function createMediaUpload(options = {}) {
41
+ const origin = (options.baseUrl ?? "").replace(/\/$/, "");
42
+ const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;
43
+ const baseFetch = /* @__PURE__ */ __name(() => options.fetch ?? fetch, "baseFetch");
44
+ const fetchWithCredentials = /* @__PURE__ */ __name((input, init) => baseFetch()(input, {
45
+ ...init,
46
+ ...options.credentials !== void 0 ? {
47
+ credentials: options.credentials
48
+ } : {}
49
+ }), "fetchWithCredentials");
50
+ const headers = /* @__PURE__ */ __name(async () => ({
51
+ ...options.headers,
52
+ ...await options.getHeaders?.() ?? {}
53
+ }), "headers");
54
+ const discard = /* @__PURE__ */ __name((mediaId) => {
55
+ void (async () => {
56
+ await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {
57
+ method: "DELETE",
58
+ headers: await headers()
59
+ });
60
+ })().catch(() => void 0);
61
+ }, "discard");
62
+ const call = /* @__PURE__ */ __name(async (method, url, body) => {
63
+ const response = await fetchWithCredentials(url, {
64
+ method,
65
+ headers: {
66
+ accept: "application/json",
67
+ ...body !== void 0 ? {
68
+ "content-type": "application/json"
69
+ } : {},
70
+ ...await headers()
71
+ },
72
+ ...body !== void 0 ? {
73
+ body: JSON.stringify(body)
74
+ } : {}
75
+ });
76
+ if (!response.ok) {
77
+ throw new MediaUploadError(response.status, method, url, response.statusText);
78
+ }
79
+ return await response.json();
80
+ }, "call");
81
+ return async (file, { signal, onProgress } = {}) => {
82
+ signal?.throwIfAborted();
83
+ const begun = await call("POST", base, {
84
+ filename: file.name,
85
+ contentType: file.type,
86
+ size: file.size
87
+ });
88
+ const onAbort = /* @__PURE__ */ __name(() => discard(begun.mediaId), "onAbort");
89
+ signal?.addEventListener("abort", onAbort, {
90
+ once: true
91
+ });
92
+ try {
93
+ const location = /^https?:\/\//.test(begun.location) ? begun.location : `${origin}${begun.location}`;
94
+ await streamChunks(location, file, {
95
+ resume: false,
96
+ fetchImpl: fetchWithCredentials,
97
+ getHeaders: headers,
98
+ ...options.chunkSize !== void 0 ? {
99
+ chunkSize: options.chunkSize
100
+ } : {},
101
+ ...options.retries !== void 0 ? {
102
+ retries: options.retries
103
+ } : {},
104
+ ...signal !== void 0 ? {
105
+ signal
106
+ } : {},
107
+ onProgress: /* @__PURE__ */ __name((sent, total) => onProgress?.(total > 0 ? sent / total : 1), "onProgress")
108
+ });
109
+ signal?.throwIfAborted();
110
+ const attachment = await call("POST", `${base}/${encodeURIComponent(begun.mediaId)}/complete`);
111
+ onProgress?.(1);
112
+ return attachment;
113
+ } catch (error) {
114
+ if (!signal?.aborted) discard(begun.mediaId);
115
+ throw error;
116
+ } finally {
117
+ signal?.removeEventListener("abort", onAbort);
118
+ }
119
+ };
120
+ }
121
+ __name(createMediaUpload, "createMediaUpload");
122
+ export {
123
+ MediaUploadError,
124
+ createMediaUpload,
125
+ mediaAttachments
126
+ };
127
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/media/index.ts","../src/client.ts"],"sourcesContent":["import type { MessageAttachment } from '@dudousxd/nestjs-agent-core';\nimport { streamChunks } from '@dudousxd/nestjs-media-client';\nimport type { AttachmentUploadStrategy, UploadAttachmentOptions } from '../backend.js';\nimport { type AgentClientOptions, normalizeAgentPath } from '../client.js';\n\n/** Tuning for {@link mediaAttachments}; every field optional. */\nexport interface MediaAttachmentsOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB. */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/**\n * Resumable attachment uploads through `@dudousxd/nestjs-media`, in one line:\n *\n * ```tsx\n * <AgentProvider attachments={{ upload: mediaAttachments() }}>\n * ```\n *\n * Reuses the client's own connection (origin, path, headers, credentials), so it needs no\n * configuration. Also takes `new AgentClient({ attachments: { upload: mediaAttachments() } })`.\n */\nexport function mediaAttachments(options: MediaAttachmentsOptions = {}): AttachmentUploadStrategy {\n return (file, uploadOptions, connection) =>\n createMediaUpload({\n ...options,\n baseUrl: connection.baseUrl,\n path: connection.path,\n getHeaders: connection.headers,\n fetch: connection.fetch,\n ...(connection.credentials !== undefined ? { credentials: connection.credentials } : {}),\n })(file, uploadOptions);\n}\n\n/**\n * Where and how {@link createMediaUpload} talks to the server. The connection fields are\n * {@link AgentClientOptions}' — pass the same ones your `AgentClient` gets, so the tus requests\n * carry the same cookies / CSRF header / bearer token as the rest of the chat.\n */\nexport interface MediaUploadOptions extends AgentClientOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB (nestjs-media-client's default). */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/** What `useAttachments({ upload })` and `AgentBackend.uploadAttachment` both take. */\nexport type AttachmentUpload = (\n file: File,\n options?: UploadAttachmentOptions,\n) => Promise<MessageAttachment>;\n\n/**\n * A non-2xx answer from the agent's upload routes. Carries `status` like `AgentHttpError` does\n * (413 too large, 415 type refused, 404 not yours) — its own class because this subpath is bundled\n * apart from the root entry, where an `instanceof AgentHttpError` would not match a copy.\n */\nexport class MediaUploadError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n ) {\n super(`Attachment upload failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'MediaUploadError';\n }\n}\n\ninterface BeginResponse {\n mediaId: string;\n location: string;\n}\n\n/**\n * A resumable `upload` for `useAttachments`, backed by `@dudousxd/nestjs-media` on the server\n * (`AgentMediaAttachmentsModule` from `@dudousxd/nestjs-agent/media`):\n *\n * 1. `POST <path>/attachments/uploads` — the agent validates the file and opens a tus session the\n * actor owns;\n * 2. the bytes stream to nestjs-media's own tus endpoint in chunks (`streamChunks`), reporting\n * progress and honouring the abort signal;\n * 3. `POST <path>/attachments/uploads/:mediaId/complete` — the agent confirms the bytes landed and\n * answers the `MessageAttachment` the turn will reference by `mediaId`.\n *\n * An aborted or failed upload is discarded server-side (`DELETE`), best effort.\n */\nexport function createMediaUpload(options: MediaUploadOptions = {}): AttachmentUpload {\n const origin = (options.baseUrl ?? '').replace(/\\/$/, '');\n const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;\n const baseFetch = (): typeof fetch => options.fetch ?? fetch;\n // Every request — the agent's and media's tus PATCHes — rides the same credentials mode.\n const fetchWithCredentials: typeof fetch = (input, init) =>\n baseFetch()(input, {\n ...init,\n ...(options.credentials !== undefined ? { credentials: options.credentials } : {}),\n });\n const headers = async (): Promise<Record<string, string>> => ({\n ...options.headers,\n ...((await options.getHeaders?.()) ?? {}),\n });\n const discard = (mediaId: string): void => {\n void (async () => {\n await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {\n method: 'DELETE',\n headers: await headers(),\n });\n })().catch(() => undefined);\n };\n const call = async <T>(method: string, url: string, body?: unknown): Promise<T> => {\n const response = await fetchWithCredentials(url, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await headers()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n if (!response.ok) {\n throw new MediaUploadError(response.status, method, url, response.statusText);\n }\n return (await response.json()) as T;\n };\n\n return async (file, { signal, onProgress } = {}) => {\n signal?.throwIfAborted();\n const begun = await call<BeginResponse>('POST', base, {\n filename: file.name,\n contentType: file.type,\n size: file.size,\n });\n const onAbort = () => discard(begun.mediaId);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n const location = /^https?:\\/\\//.test(begun.location)\n ? begun.location\n : `${origin}${begun.location}`;\n await streamChunks(location, file, {\n resume: false,\n fetchImpl: fetchWithCredentials,\n getHeaders: headers,\n ...(options.chunkSize !== undefined ? { chunkSize: options.chunkSize } : {}),\n ...(options.retries !== undefined ? { retries: options.retries } : {}),\n ...(signal !== undefined ? { signal } : {}),\n onProgress: (sent, total) => onProgress?.(total > 0 ? sent / total : 1),\n });\n signal?.throwIfAborted();\n const attachment = await call<MessageAttachment>(\n 'POST',\n `${base}/${encodeURIComponent(begun.mediaId)}/complete`,\n );\n onProgress?.(1);\n return attachment;\n } catch (error) {\n if (!signal?.aborted) discard(begun.mediaId);\n throw error;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n","import type {\n AgentCatalogEntry,\n MessageAttachment,\n MessageFeedback,\n ModelCatalogView,\n QuotaReport,\n QuotaView,\n SkillCatalogEntry,\n ThreadDetail,\n ThreadSummary,\n ToolCatalogEntry,\n} from '@dudousxd/nestjs-agent-core';\nimport type {\n AgentBackend,\n AgentConnection,\n AttachmentUploadStrategy,\n ChatStreamRequest,\n ChatStreamResponse,\n MessageFeedbackInput,\n ResumeStreamRequest,\n ThreadPatch,\n UploadAttachmentOptions,\n} from './backend.js';\n\nexport type { ThreadPatch } from './backend.js';\n\n/**\n * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can\n * branch (e.g. 403 → \"not your thread\", 429 → quota) instead of string-matching a generic Error.\n */\nexport class AgentHttpError extends Error {\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n ) {\n super(`Agent request failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'AgentHttpError';\n }\n}\n\n/** The quota-today read-model: usage, the configured limit (null → unlimited), and USD spend. */\nexport type QuotaToday = QuotaView;\n\nexport interface CancelResult {\n aborted: boolean;\n}\n\nexport interface OkResult {\n ok: boolean;\n}\n\nexport interface AgentClientOptions {\n /**\n * The server's origin, e.g. `https://api.example.com`. Defaults to `''` (same origin). The\n * agent's route prefix is {@link AgentClientOptions.path}, not part of this.\n */\n baseUrl?: string;\n /**\n * The agent's route prefix — `AgentModule`'s `path`, with any global prefix in front\n * (`'api/agent'`). Leading/trailing slashes are optional. Defaults to `'agent'`.\n */\n path?: string;\n /** Static headers merged into every request. */\n headers?: Record<string, string>;\n /**\n * Resolved per request — for short-lived bearer tokens, or a CSRF header read from a cookie\n * (`{ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }`), which has to be read at request time because\n * the server may rotate it.\n */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /**\n * Forwarded to fetch so cookie auth works. Same-origin requests send cookies by default; set\n * `'include'` when the API lives on another origin (and have it answer with credentialed CORS).\n */\n credentials?: RequestCredentials;\n /** Injectable for tests / non-browser runtimes. */\n fetch?: typeof fetch;\n /** Attachment uploads. */\n attachments?: {\n /**\n * How `uploadAttachment` uploads. Omitted → `POST <path>/attachments` (multipart). Pass\n * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through\n * nestjs-media, or your own {@link AttachmentUploadStrategy}.\n */\n upload?: AttachmentUploadStrategy;\n };\n}\n\n/** `'/api/agent'` from `'api/agent'`, `'/api/agent/'`, …; `''` for an empty path. */\nexport function normalizeAgentPath(path: string | undefined): string {\n const trimmed = (path ?? 'agent').replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `/${trimmed}`;\n}\n\nconst HEADER_RUN_ID = 'x-agent-run-id';\nconst HEADER_THREAD_ID = 'x-agent-thread-id';\n\n/**\n * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.\n * Used by `useAgentChat`, but standalone-usable (vanilla fetch, no React).\n */\nexport class AgentClient implements AgentBackend {\n constructor(private readonly options: AgentClientOptions = {}) {}\n\n /** `POST <path>/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */\n async openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse> {\n const response = await this.fetchImpl()(`${this.root()}/chat`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n body: JSON.stringify(request.body),\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (!response.ok || !response.body) {\n throw new AgentHttpError(\n response.status,\n 'POST',\n `${this.agentPath()}/chat`,\n response.statusText,\n );\n }\n return streamResponse(response);\n }\n\n /**\n * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is\n * streaming under that id (404).\n */\n async resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null> {\n const path = `/chat/${encodeURIComponent(request.runId)}/stream`;\n const query = request.after !== undefined && request.after > 0 ? `?after=${request.after}` : '';\n const response = await this.fetchImpl()(`${this.root()}${path}${query}`, {\n method: 'GET',\n headers: {\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 404) {\n return null;\n }\n if (!response.ok || !response.body) {\n throw new AgentHttpError(\n response.status,\n 'GET',\n `${this.agentPath()}${path}`,\n response.statusText,\n );\n }\n return streamResponse(response);\n }\n\n /**\n * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers\n * the stored rating.\n */\n setMessageFeedback(\n messageId: string,\n input: MessageFeedbackInput,\n ): Promise<{ feedback: MessageFeedback | null }> {\n return this.request<{ feedback: MessageFeedback | null }>(\n 'POST',\n `/messages/${encodeURIComponent(messageId)}/feedback`,\n input,\n );\n }\n\n listThreads(): Promise<ThreadSummary[]> {\n return this.request<ThreadSummary[]>('GET', '/threads');\n }\n\n /**\n * The skills this caller can invoke right now, scope-resolved — the same list, built by the same\n * call, that the model is offered, so what a user can type after a `/` and what the agent can\n * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to\n * one conversation; omitted, the server reads it as a brand-new thread.\n */\n listSkills(threadId?: string): Promise<SkillCatalogEntry[]> {\n const query = threadId === undefined ? '' : `?threadId=${encodeURIComponent(threadId)}`;\n return this.request<SkillCatalogEntry[]>('GET', `/skills${query}`);\n }\n\n /**\n * The tools this caller can reach through `agent` (the default agent when omitted), each with the\n * server-declared `presentation` a chat narrates it by — the same list the model is offered.\n * Prefer {@link useToolCatalog}, which fetches it once and shares it.\n */\n listTools(agent?: string): Promise<ToolCatalogEntry[]> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ToolCatalogEntry[]>('GET', `/tools${query}`);\n }\n\n getThread(id: string): Promise<ThreadDetail> {\n return this.request<ThreadDetail>('GET', `/threads/${encodeURIComponent(id)}`);\n }\n\n deleteThread(id: string): Promise<void> {\n return this.request<void>('DELETE', `/threads/${encodeURIComponent(id)}`);\n }\n\n forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary> {\n return this.request<ThreadSummary>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`,\n );\n }\n\n renameThread(id: string, title: string): Promise<OkResult> {\n return this.updateThread(id, { title });\n }\n\n /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */\n updateThread(id: string, patch: ThreadPatch): Promise<OkResult> {\n return this.request<OkResult>('PATCH', `/threads/${encodeURIComponent(id)}`, patch);\n }\n\n /**\n * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —\n * mirrors the backend's `POST <path>/attachments`. The returned {@link MessageAttachment} is\n * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.\n */\n async uploadAttachment(\n file: File,\n options: UploadAttachmentOptions = {},\n ): Promise<MessageAttachment> {\n const upload = this.options.attachments?.upload;\n if (upload !== undefined) {\n return upload(file, options, this.connection());\n }\n // `fetch` cannot observe an upload's progress; XHR can. Only when someone is listening, and\n // never when a `fetch` was injected (tests, non-browser runtimes).\n if (\n options.onProgress !== undefined &&\n this.options.fetch === undefined &&\n typeof XMLHttpRequest !== 'undefined'\n ) {\n return this.uploadWithProgress(file, options);\n }\n const formData = new FormData();\n formData.append('file', file);\n const response = await this.fetchImpl()(`${this.root()}/attachments`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n },\n body: formData,\n ...this.credentials(),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n });\n const attachment = await this.handleResponse<MessageAttachment>(\n response,\n 'POST',\n `${this.agentPath()}/attachments`,\n );\n options.onProgress?.(1);\n return attachment;\n }\n\n private async uploadWithProgress(\n file: File,\n { signal, onProgress }: UploadAttachmentOptions,\n ): Promise<MessageAttachment> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n };\n const url = `${this.root()}/attachments`;\n return new Promise<MessageAttachment>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', url);\n for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value);\n // Same-origin requests carry cookies regardless; this is the cross-origin opt-in.\n xhr.withCredentials = this.options.credentials === 'include';\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable && event.total > 0) onProgress?.(event.loaded / event.total);\n };\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(\n new AgentHttpError(\n xhr.status,\n 'POST',\n `${this.agentPath()}/attachments`,\n xhr.statusText,\n ),\n );\n return;\n }\n onProgress?.(1);\n resolve(JSON.parse(xhr.responseText) as MessageAttachment);\n };\n xhr.onerror = () => reject(new TypeError('Network error while uploading the attachment'));\n xhr.onabort = () => reject(new DOMException('The upload was aborted', 'AbortError'));\n if (signal !== undefined) {\n if (signal.aborted) {\n reject(new DOMException('The upload was aborted', 'AbortError'));\n return;\n }\n signal.addEventListener('abort', () => xhr.abort(), { once: true });\n }\n const formData = new FormData();\n formData.append('file', file);\n xhr.send(formData);\n });\n }\n\n promoteThread(id: string): Promise<OkResult> {\n return this.request<OkResult>('POST', `/threads/${encodeURIComponent(id)}/promote`);\n }\n\n truncateFromMessage(threadId: string, messageId: string): Promise<OkResult> {\n return this.request<OkResult>(\n 'DELETE',\n `/threads/${encodeURIComponent(threadId)}/from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** `GET <path>/models?agent=` — the models this caller may pick, grouped by provider. */\n listModels(agent?: string): Promise<ModelCatalogView> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ModelCatalogView>('GET', `/models${query}`);\n }\n\n /** `GET <path>/agents` — the registered agents, the default one flagged. */\n listAgents(): Promise<AgentCatalogEntry[]> {\n return this.request<AgentCatalogEntry[]>('GET', '/agents');\n }\n\n /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */\n getQuota(): Promise<QuotaReport> {\n return this.request<QuotaReport>('GET', '/quota');\n }\n\n getQuotaToday(): Promise<QuotaToday> {\n return this.request<QuotaToday>('GET', '/quota/today');\n }\n\n cancelStream(runId: string): Promise<CancelResult> {\n return this.request<CancelResult>('POST', `/chat/${encodeURIComponent(runId)}/cancel`);\n }\n\n /**\n * `remember` approves later calls of the same tool in the same thread; `via` names the surface\n * the decision came through (the server records `'web'` when omitted).\n */\n approveToolCall(input: { toolCallId: string; remember?: boolean; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/approve', input);\n }\n\n rejectToolCall(input: { toolCallId: string; reason?: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/reject', input);\n }\n\n /**\n * Settle a parked question set. `answers` is questionId → chosen option values; a question left\n * out takes the pre-picked default the request carried, resolved server-side against the request\n * the run already holds. Omit the whole object and the user has confirmed every pre-picked\n * answer — which is the point of the surface, so it is a valid submission rather than a blank.\n */\n answerToolCall(input: {\n toolCallId: string;\n answers?: Record<string, string[]>;\n }): Promise<void> {\n return this.request<void>('POST', '/tool-call/answer', input);\n }\n\n /**\n * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same\n * values a confirmation would, and persists differently on purpose — only one of them is\n * evidence the user chose them.\n */\n skipToolCall(input: { toolCallId: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/skip', input);\n }\n\n private fetchImpl(): typeof fetch {\n return this.options.fetch ?? globalThis.fetch;\n }\n\n /** This client's connection, for an {@link AttachmentUploadStrategy}. */\n private connection(): AgentConnection {\n return {\n baseUrl: this.baseUrl(),\n path: this.agentPath(),\n headers: () => this.resolveHeaders(),\n fetch: this.fetchImpl(),\n ...this.credentials(),\n };\n }\n\n private baseUrl(): string {\n return (this.options.baseUrl ?? '').replace(/\\/+$/, '');\n }\n\n private agentPath(): string {\n return normalizeAgentPath(this.options.path);\n }\n\n /** Origin + agent path: what every route hangs off. */\n private root(): string {\n return `${this.baseUrl()}${this.agentPath()}`;\n }\n\n private async resolveHeaders(): Promise<Record<string, string>> {\n const dynamic = (await this.options.getHeaders?.()) ?? {};\n return { ...this.options.headers, ...dynamic };\n }\n\n private credentials(): { credentials?: RequestCredentials } {\n return this.options.credentials !== undefined ? { credentials: this.options.credentials } : {};\n }\n\n private async request<T>(method: string, route: string, body?: unknown): Promise<T> {\n const path = `${this.agentPath()}${route}`;\n const response = await this.fetchImpl()(`${this.baseUrl()}${path}`, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await this.resolveHeaders()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n ...this.credentials(),\n });\n return this.handleResponse<T>(response, method, path);\n }\n\n private async handleResponse<T>(response: Response, method: string, path: string): Promise<T> {\n if (!response.ok) {\n throw new AgentHttpError(response.status, method, path, response.statusText);\n }\n if (response.status === 204) return undefined as T;\n const text = await response.text();\n if (!text) return undefined as T;\n return JSON.parse(text) as T;\n }\n}\n\nfunction streamResponse(response: Response): ChatStreamResponse {\n const body = response.body as ReadableStream<Uint8Array>;\n const runId = response.headers?.get(HEADER_RUN_ID) ?? undefined;\n const threadId = response.headers?.get(HEADER_THREAD_ID) ?? undefined;\n return {\n body,\n ...(runId ? { runId } : {}),\n ...(threadId ? { threadId } : {}),\n };\n}\n"],"mappings":"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","names":["streamChunks","normalizeAgentPath","path","trimmed","replace","mediaAttachments","options","file","uploadOptions","connection","createMediaUpload","baseUrl","path","getHeaders","headers","fetch","credentials","undefined","MediaUploadError","Error","status","method","statusText","name","origin","replace","base","normalizeAgentPath","baseFetch","fetchWithCredentials","input","init","discard","mediaId","encodeURIComponent","catch","call","url","body","response","accept","JSON","stringify","ok","json","signal","onProgress","throwIfAborted","begun","filename","contentType","type","size","onAbort","addEventListener","once","location","test","streamChunks","resume","fetchImpl","chunkSize","retries","sent","total","attachment","error","aborted","removeEventListener"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-agent-react",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/DavideCarvalho/nestjs-agent.git",
@@ -67,6 +67,16 @@
67
67
  "types": "./dist/genui-json-render.d.cts",
68
68
  "default": "./dist/genui-json-render.cjs"
69
69
  }
70
+ },
71
+ "./media": {
72
+ "import": {
73
+ "types": "./dist/media.d.ts",
74
+ "default": "./dist/media.js"
75
+ },
76
+ "require": {
77
+ "types": "./dist/media.d.cts",
78
+ "default": "./dist/media.cjs"
79
+ }
70
80
  }
71
81
  },
72
82
  "files": [
@@ -77,6 +87,7 @@
77
87
  },
78
88
  "peerDependencies": {
79
89
  "@ai-sdk/react": "^4.0.0",
90
+ "@dudousxd/nestjs-media-client": ">=0.3.0",
80
91
  "@json-render/react": ">=0.21.0 <1.0.0",
81
92
  "@streamdown/code": "^1.1.1",
82
93
  "@streamdown/math": "^1.0.2",
@@ -94,6 +105,9 @@
94
105
  "unist-util-visit": "^5.1.0"
95
106
  },
96
107
  "peerDependenciesMeta": {
108
+ "@dudousxd/nestjs-media-client": {
109
+ "optional": true
110
+ },
97
111
  "@json-render/react": {
98
112
  "optional": true
99
113
  },
@@ -136,6 +150,7 @@
136
150
  },
137
151
  "devDependencies": {
138
152
  "@ai-sdk/react": "4.0.17",
153
+ "@dudousxd/nestjs-media-client": "0.3.0",
139
154
  "@json-render/core": "0.21.0",
140
155
  "@json-render/react": "0.21.0",
141
156
  "@streamdown/code": "1.1.1",