@dudousxd/nestjs-agent-react 0.26.0 → 0.28.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/README.md +51 -1
- package/dist/{client-D4ZSbZJ8.d.cts → client-CKwSMitS.d.cts} +88 -2
- package/dist/{client-D4ZSbZJ8.d.ts → client-CKwSMitS.d.ts} +88 -2
- package/dist/{generative-ui-BejHxxbR.d.cts → generative-ui-jT9TYfCi.d.cts} +1 -1
- package/dist/{generative-ui-BejHxxbR.d.ts → generative-ui-jT9TYfCi.d.ts} +1 -1
- package/dist/genui-json-render.cjs +202 -370
- package/dist/genui-json-render.cjs.map +1 -1
- package/dist/genui-json-render.d.cts +1 -1
- package/dist/genui-json-render.d.ts +1 -1
- package/dist/genui-json-render.js +209 -373
- package/dist/genui-json-render.js.map +1 -1
- package/dist/genui.cjs +178 -320
- package/dist/genui.cjs.map +1 -1
- package/dist/genui.d.cts +2 -2
- package/dist/genui.d.ts +2 -2
- package/dist/genui.js +185 -323
- package/dist/genui.js.map +1 -1
- package/dist/index.cjs +2266 -2479
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +193 -13
- package/dist/index.d.ts +193 -13
- package/dist/index.js +2292 -2487
- package/dist/index.js.map +1 -1
- package/dist/markdown.cjs +23 -36
- package/dist/markdown.cjs.map +1 -1
- package/dist/markdown.js +23 -28
- package/dist/markdown.js.map +1 -1
- package/dist/media.cjs +46 -79
- package/dist/media.cjs.map +1 -1
- package/dist/media.d.cts +1 -1
- package/dist/media.d.ts +1 -1
- package/dist/media.js +46 -81
- package/dist/media.js.map +1 -1
- package/package.json +3 -2
package/dist/media.js
CHANGED
|
@@ -1,41 +1,22 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
-
|
|
4
1
|
// src/media/index.ts
|
|
5
2
|
import { streamChunks } from "@dudousxd/nestjs-media-client";
|
|
6
3
|
|
|
7
4
|
// src/http-error.ts
|
|
8
5
|
function readErrorAnswer(text) {
|
|
9
|
-
if (text == null || text === "") return {
|
|
10
|
-
body: void 0,
|
|
11
|
-
message: void 0,
|
|
12
|
-
code: void 0
|
|
13
|
-
};
|
|
6
|
+
if (text == null || text === "") return { body: void 0, message: void 0, code: void 0 };
|
|
14
7
|
let body = text;
|
|
15
8
|
try {
|
|
16
9
|
body = JSON.parse(text);
|
|
17
10
|
} catch {
|
|
18
|
-
return {
|
|
19
|
-
body: text,
|
|
20
|
-
message: void 0,
|
|
21
|
-
code: void 0
|
|
22
|
-
};
|
|
11
|
+
return { body: text, message: void 0, code: void 0 };
|
|
23
12
|
}
|
|
24
|
-
if (body === null || typeof body !== "object")
|
|
25
|
-
body,
|
|
26
|
-
message: void 0,
|
|
27
|
-
code: void 0
|
|
28
|
-
};
|
|
13
|
+
if (body === null || typeof body !== "object")
|
|
14
|
+
return { body, message: void 0, code: void 0 };
|
|
29
15
|
const record = body;
|
|
30
16
|
const message = typeof record.message === "string" && record.message !== "" ? record.message : Array.isArray(record.message) && record.message.length > 0 && record.message.every((each) => typeof each === "string") ? record.message.join("; ") : void 0;
|
|
31
17
|
const code = typeof record.code === "string" && record.code !== "" ? record.code : void 0;
|
|
32
|
-
return {
|
|
33
|
-
body,
|
|
34
|
-
message,
|
|
35
|
-
code
|
|
36
|
-
};
|
|
18
|
+
return { body, message, code };
|
|
37
19
|
}
|
|
38
|
-
__name(readErrorAnswer, "readErrorAnswer");
|
|
39
20
|
async function readErrorResponse(response) {
|
|
40
21
|
let text;
|
|
41
22
|
try {
|
|
@@ -45,7 +26,6 @@ async function readErrorResponse(response) {
|
|
|
45
26
|
}
|
|
46
27
|
return readErrorAnswer(text);
|
|
47
28
|
}
|
|
48
|
-
__name(readErrorResponse, "readErrorResponse");
|
|
49
29
|
function reportHttpError(listener, error) {
|
|
50
30
|
if (listener === void 0) return;
|
|
51
31
|
try {
|
|
@@ -53,14 +33,12 @@ function reportHttpError(listener, error) {
|
|
|
53
33
|
} catch {
|
|
54
34
|
}
|
|
55
35
|
}
|
|
56
|
-
__name(reportHttpError, "reportHttpError");
|
|
57
36
|
|
|
58
37
|
// src/client.ts
|
|
59
38
|
function normalizeAgentPath(path) {
|
|
60
39
|
const trimmed = (path ?? "agent").replace(/^\/+|\/+$/g, "");
|
|
61
40
|
return trimmed === "" ? "" : `/${trimmed}`;
|
|
62
41
|
}
|
|
63
|
-
__name(normalizeAgentPath, "normalizeAgentPath");
|
|
64
42
|
|
|
65
43
|
// src/media/index.ts
|
|
66
44
|
function mediaAttachments(options = {}) {
|
|
@@ -70,78 +48,71 @@ function mediaAttachments(options = {}) {
|
|
|
70
48
|
path: connection.path,
|
|
71
49
|
getHeaders: connection.headers,
|
|
72
50
|
fetch: connection.fetch,
|
|
73
|
-
...connection.credentials !== void 0 ? {
|
|
74
|
-
|
|
75
|
-
} : {},
|
|
76
|
-
...connection.onHttpError !== void 0 ? {
|
|
77
|
-
onHttpError: connection.onHttpError
|
|
78
|
-
} : {}
|
|
51
|
+
...connection.credentials !== void 0 ? { credentials: connection.credentials } : {},
|
|
52
|
+
...connection.onHttpError !== void 0 ? { onHttpError: connection.onHttpError } : {}
|
|
79
53
|
})(file, uploadOptions);
|
|
80
54
|
}
|
|
81
|
-
__name(mediaAttachments, "mediaAttachments");
|
|
82
55
|
var MediaUploadError = class extends Error {
|
|
83
|
-
|
|
84
|
-
|
|
56
|
+
constructor(status, method, path, statusText, answer = { body: void 0, message: void 0, code: void 0 }) {
|
|
57
|
+
super(
|
|
58
|
+
answer.message ?? `Attachment upload failed: ${method} ${path} \u2192 ${status} ${statusText}`
|
|
59
|
+
);
|
|
60
|
+
this.status = status;
|
|
61
|
+
this.method = method;
|
|
62
|
+
this.path = path;
|
|
63
|
+
this.name = "MediaUploadError";
|
|
64
|
+
this.body = answer.body;
|
|
65
|
+
this.code = answer.code;
|
|
85
66
|
}
|
|
86
67
|
status;
|
|
87
68
|
method;
|
|
88
69
|
path;
|
|
89
70
|
body;
|
|
90
71
|
code;
|
|
91
|
-
constructor(status, method, path, statusText, answer = {
|
|
92
|
-
body: void 0,
|
|
93
|
-
message: void 0,
|
|
94
|
-
code: void 0
|
|
95
|
-
}) {
|
|
96
|
-
super(answer.message ?? `Attachment upload failed: ${method} ${path} \u2192 ${status} ${statusText}`), this.status = status, this.method = method, this.path = path;
|
|
97
|
-
this.name = "MediaUploadError";
|
|
98
|
-
this.body = answer.body;
|
|
99
|
-
this.code = answer.code;
|
|
100
|
-
}
|
|
101
72
|
};
|
|
102
73
|
function createMediaUpload(options = {}) {
|
|
103
74
|
const origin = (options.baseUrl ?? "").replace(/\/$/, "");
|
|
104
75
|
const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;
|
|
105
|
-
const baseFetch =
|
|
106
|
-
const fetchWithCredentials =
|
|
76
|
+
const baseFetch = () => options.fetch ?? fetch;
|
|
77
|
+
const fetchWithCredentials = (input, init) => baseFetch()(input, {
|
|
107
78
|
...init,
|
|
108
|
-
...options.credentials !== void 0 ? {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}), "fetchWithCredentials");
|
|
112
|
-
const headers = /* @__PURE__ */ __name(async () => ({
|
|
79
|
+
...options.credentials !== void 0 ? { credentials: options.credentials } : {}
|
|
80
|
+
});
|
|
81
|
+
const headers = async () => ({
|
|
113
82
|
...options.headers,
|
|
114
83
|
...await options.getHeaders?.() ?? {}
|
|
115
|
-
})
|
|
116
|
-
const discard =
|
|
84
|
+
});
|
|
85
|
+
const discard = (mediaId) => {
|
|
117
86
|
void (async () => {
|
|
118
87
|
await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {
|
|
119
88
|
method: "DELETE",
|
|
120
89
|
headers: await headers()
|
|
121
90
|
});
|
|
122
91
|
})().catch(() => void 0);
|
|
123
|
-
}
|
|
124
|
-
const call =
|
|
92
|
+
};
|
|
93
|
+
const call = async (method, url, body) => {
|
|
125
94
|
const response = await fetchWithCredentials(url, {
|
|
126
95
|
method,
|
|
127
96
|
headers: {
|
|
128
97
|
accept: "application/json",
|
|
129
|
-
...body !== void 0 ? {
|
|
130
|
-
"content-type": "application/json"
|
|
131
|
-
} : {},
|
|
98
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
132
99
|
...await headers()
|
|
133
100
|
},
|
|
134
|
-
...body !== void 0 ? {
|
|
135
|
-
body: JSON.stringify(body)
|
|
136
|
-
} : {}
|
|
101
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
137
102
|
});
|
|
138
103
|
if (!response.ok) {
|
|
139
|
-
const error = new MediaUploadError(
|
|
104
|
+
const error = new MediaUploadError(
|
|
105
|
+
response.status,
|
|
106
|
+
method,
|
|
107
|
+
url,
|
|
108
|
+
response.statusText,
|
|
109
|
+
await readErrorResponse(response)
|
|
110
|
+
);
|
|
140
111
|
reportHttpError(options.onHttpError, error);
|
|
141
112
|
throw error;
|
|
142
113
|
}
|
|
143
114
|
return await response.json();
|
|
144
|
-
}
|
|
115
|
+
};
|
|
145
116
|
return async (file, { signal, onProgress } = {}) => {
|
|
146
117
|
signal?.throwIfAborted();
|
|
147
118
|
const begun = await call("POST", base, {
|
|
@@ -149,29 +120,24 @@ function createMediaUpload(options = {}) {
|
|
|
149
120
|
contentType: file.type,
|
|
150
121
|
size: file.size
|
|
151
122
|
});
|
|
152
|
-
const onAbort =
|
|
153
|
-
signal?.addEventListener("abort", onAbort, {
|
|
154
|
-
once: true
|
|
155
|
-
});
|
|
123
|
+
const onAbort = () => discard(begun.mediaId);
|
|
124
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
156
125
|
try {
|
|
157
126
|
const location = /^https?:\/\//.test(begun.location) ? begun.location : `${origin}${begun.location}`;
|
|
158
127
|
await streamChunks(location, file, {
|
|
159
128
|
resume: false,
|
|
160
129
|
fetchImpl: fetchWithCredentials,
|
|
161
130
|
getHeaders: headers,
|
|
162
|
-
...options.chunkSize !== void 0 ? {
|
|
163
|
-
|
|
164
|
-
} : {},
|
|
165
|
-
|
|
166
|
-
retries: options.retries
|
|
167
|
-
} : {},
|
|
168
|
-
...signal !== void 0 ? {
|
|
169
|
-
signal
|
|
170
|
-
} : {},
|
|
171
|
-
onProgress: /* @__PURE__ */ __name((sent, total) => onProgress?.(total > 0 ? sent / total : 1), "onProgress")
|
|
131
|
+
...options.chunkSize !== void 0 ? { chunkSize: options.chunkSize } : {},
|
|
132
|
+
...options.retries !== void 0 ? { retries: options.retries } : {},
|
|
133
|
+
...signal !== void 0 ? { signal } : {},
|
|
134
|
+
onProgress: (sent, total) => onProgress?.(total > 0 ? sent / total : 1)
|
|
172
135
|
});
|
|
173
136
|
signal?.throwIfAborted();
|
|
174
|
-
const attachment = await call(
|
|
137
|
+
const attachment = await call(
|
|
138
|
+
"POST",
|
|
139
|
+
`${base}/${encodeURIComponent(begun.mediaId)}/complete`
|
|
140
|
+
);
|
|
175
141
|
onProgress?.(1);
|
|
176
142
|
return attachment;
|
|
177
143
|
} catch (error) {
|
|
@@ -182,7 +148,6 @@ function createMediaUpload(options = {}) {
|
|
|
182
148
|
}
|
|
183
149
|
};
|
|
184
150
|
}
|
|
185
|
-
__name(createMediaUpload, "createMediaUpload");
|
|
186
151
|
export {
|
|
187
152
|
MediaUploadError,
|
|
188
153
|
createMediaUpload,
|
package/dist/media.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/media/index.ts","../src/http-error.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';\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from '../http-error.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 ...(connection.onHttpError !== undefined ? { onHttpError: connection.onHttpError } : {}),\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 what `AgentHttpError` does — `status`\n * (413 too large, 415 type refused, 404 not yours), the server's `message`, `code` and `body` —\n * as its own class because this subpath is bundled apart from the root entry, where an\n * `instanceof AgentHttpError` would not match a copy. Both satisfy `AgentRequestError`.\n */\nexport class MediaUploadError extends Error implements AgentRequestError {\n readonly body: unknown;\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(\n answer.message ?? `Attachment upload failed: ${method} ${path} → ${status} ${statusText}`,\n );\n this.name = 'MediaUploadError';\n this.body = answer.body;\n this.code = answer.code;\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 const error = new MediaUploadError(\n response.status,\n method,\n url,\n response.statusText,\n await readErrorResponse(response),\n );\n reportHttpError(options.onHttpError, error);\n throw error;\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","/**\n * What a server said when it refused a request, read once so every error class of this package\n * reports it the same way. Bundled into each entry that throws (the root and `/media`), which is\n * why it holds no class of its own.\n */\nexport interface ErrorAnswer {\n /** The body, parsed as JSON when it is JSON, else the raw text; `undefined` when empty. */\n body: unknown;\n /** `body.message` — a string, or NestJS's list of validation messages joined with `; `. */\n message: string | undefined;\n /** `body.code`, the machine-readable reason (`quota_exceeded`, …), when the server sent one. */\n code: string | undefined;\n}\n\n/** Read an error answer's body text. Never throws: a body that cannot be read is just absent. */\nexport function readErrorAnswer(text: string | null | undefined): ErrorAnswer {\n if (text == null || text === '') return { body: undefined, message: undefined, code: undefined };\n let body: unknown = text;\n try {\n body = JSON.parse(text);\n } catch {\n return { body: text, message: undefined, code: undefined };\n }\n if (body === null || typeof body !== 'object')\n return { body, message: undefined, code: undefined };\n const record = body as { message?: unknown; code?: unknown };\n const message =\n typeof record.message === 'string' && record.message !== ''\n ? record.message\n : Array.isArray(record.message) &&\n record.message.length > 0 &&\n record.message.every((each) => typeof each === 'string')\n ? record.message.join('; ')\n : undefined;\n const code = typeof record.code === 'string' && record.code !== '' ? record.code : undefined;\n return { body, message, code };\n}\n\n/** {@link readErrorAnswer} over a `Response` whose status is not 2xx. */\nexport async function readErrorResponse(response: Response): Promise<ErrorAnswer> {\n let text: string | undefined;\n try {\n text = await response.text();\n } catch {\n text = undefined;\n }\n return readErrorAnswer(text);\n}\n\n/**\n * The shape both {@link import('./client.js').AgentHttpError} and `MediaUploadError` share, for\n * code that handles either without an `instanceof` (the two live in separate bundles).\n */\nexport interface AgentRequestError extends Error {\n status: number;\n method: string;\n path: string;\n body: unknown;\n code: string | undefined;\n}\n\n/** Called with every error answer before it is thrown — for app-wide reactions (401, 402, …). */\nexport type HttpErrorListener = (error: AgentRequestError) => void;\n\n/** Tell `listener` about `error`; a listener that throws never replaces the error itself. */\nexport function reportHttpError(\n listener: HttpErrorListener | undefined,\n error: AgentRequestError,\n): void {\n if (listener === undefined) return;\n try {\n listener(error);\n } catch {\n /* the request's own error is what the caller gets */\n }\n}\n","import type {\n AgentCatalogEntry,\n AgentClientConfig,\n MessageAttachment,\n MessageFeedback,\n ModelCatalogView,\n QuotaReport,\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\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n type HttpErrorListener,\n readErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from './http-error.js';\n\nexport type { ThreadPatch } from './backend.js';\nexport type { AgentRequestError, HttpErrorListener } from './http-error.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 * and what the server said: `message` is the body's `message` when it sent one (so a hook that\n * shows `error.message` shows the server's words), `code` its machine-readable `code`, and `body`\n * the whole answer, parsed when it is JSON.\n */\nexport class AgentHttpError extends Error implements AgentRequestError {\n /** The answer's body: parsed JSON, else its text; `undefined` when empty. */\n readonly body: unknown;\n /** The body's `code` (`quota_exceeded`, …), when the server sent one. */\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(answer.message ?? `Agent request failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'AgentHttpError';\n this.body = answer.body;\n this.code = answer.code;\n }\n\n /** Read a refused `response`'s body into an error. */\n static async from(response: Response, method: string, path: string): Promise<AgentHttpError> {\n return new AgentHttpError(\n response.status,\n method,\n path,\n response.statusText,\n await readErrorResponse(response),\n );\n }\n}\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 /**\n * Called with every error answer (an {@link AgentHttpError}, or a `MediaUploadError` from a\n * `mediaAttachments()` upload) right before it is thrown — the place for app-wide reactions such\n * as \"401 → sign in again\". The error still reaches the caller. Not called for the `404` a\n * resume answers when nothing is streaming, which is an answer, not a failure.\n */\n onHttpError?: HttpErrorListener;\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 await this.failure(response, 'POST', `${this.agentPath()}/chat`);\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 await this.failure(response, 'GET', `${this.agentPath()}${path}`);\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 /** 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 const error = new AgentHttpError(\n xhr.status,\n 'POST',\n `${this.agentPath()}/attachments`,\n xhr.statusText,\n readErrorAnswer(xhr.responseText),\n );\n reportHttpError(this.options.onHttpError, error);\n reject(error);\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>/config` — attachment limits and upload mode, and which features are on. */\n getConfig(): Promise<AgentClientConfig> {\n return this.request<AgentClientConfig>('GET', '/config');\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 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 * `via` names the surface the answer came through (the server records `'web'` when omitted).\n */\n answerToolCall(input: {\n toolCallId: string;\n answers?: Record<string, string[]>;\n via?: 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; via?: 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 ...(this.options.onHttpError !== undefined ? { onHttpError: this.options.onHttpError } : {}),\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 /** The error for a refused `response`, already reported to `onHttpError`. */\n private async failure(response: Response, method: string, path: string): Promise<AgentHttpError> {\n const error = await AgentHttpError.from(response, method, path);\n reportHttpError(this.options.onHttpError, error);\n return error;\n }\n\n private async handleResponse<T>(response: Response, method: string, path: string): Promise<T> {\n if (!response.ok) {\n throw await this.failure(response, method, path);\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","readErrorAnswer","text","body","undefined","message","code","JSON","parse","record","Array","isArray","length","every","each","join","readErrorResponse","response","reportHttpError","listener","error","normalizeAgentPath","path","trimmed","replace","mediaAttachments","options","file","uploadOptions","connection","createMediaUpload","baseUrl","path","getHeaders","headers","fetch","credentials","undefined","onHttpError","MediaUploadError","Error","body","code","status","method","statusText","answer","message","name","origin","replace","base","normalizeAgentPath","baseFetch","fetchWithCredentials","input","init","discard","mediaId","encodeURIComponent","catch","call","url","response","accept","JSON","stringify","ok","error","readErrorResponse","reportHttpError","json","signal","onProgress","throwIfAborted","begun","filename","contentType","type","size","onAbort","addEventListener","once","location","test","streamChunks","resume","fetchImpl","chunkSize","retries","sent","total","attachment","aborted","removeEventListener"]}
|
|
1
|
+
{"version":3,"sources":["../src/media/index.ts","../src/http-error.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';\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from '../http-error.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 ...(connection.onHttpError !== undefined ? { onHttpError: connection.onHttpError } : {}),\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 what `AgentHttpError` does — `status`\n * (413 too large, 415 type refused, 404 not yours), the server's `message`, `code` and `body` —\n * as its own class because this subpath is bundled apart from the root entry, where an\n * `instanceof AgentHttpError` would not match a copy. Both satisfy `AgentRequestError`.\n */\nexport class MediaUploadError extends Error implements AgentRequestError {\n readonly body: unknown;\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(\n answer.message ?? `Attachment upload failed: ${method} ${path} → ${status} ${statusText}`,\n );\n this.name = 'MediaUploadError';\n this.body = answer.body;\n this.code = answer.code;\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 const error = new MediaUploadError(\n response.status,\n method,\n url,\n response.statusText,\n await readErrorResponse(response),\n );\n reportHttpError(options.onHttpError, error);\n throw error;\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","/**\n * What a server said when it refused a request, read once so every error class of this package\n * reports it the same way. Bundled into each entry that throws (the root and `/media`), which is\n * why it holds no class of its own.\n */\nexport interface ErrorAnswer {\n /** The body, parsed as JSON when it is JSON, else the raw text; `undefined` when empty. */\n body: unknown;\n /** `body.message` — a string, or NestJS's list of validation messages joined with `; `. */\n message: string | undefined;\n /** `body.code`, the machine-readable reason (`quota_exceeded`, …), when the server sent one. */\n code: string | undefined;\n}\n\n/** Read an error answer's body text. Never throws: a body that cannot be read is just absent. */\nexport function readErrorAnswer(text: string | null | undefined): ErrorAnswer {\n if (text == null || text === '') return { body: undefined, message: undefined, code: undefined };\n let body: unknown = text;\n try {\n body = JSON.parse(text);\n } catch {\n return { body: text, message: undefined, code: undefined };\n }\n if (body === null || typeof body !== 'object')\n return { body, message: undefined, code: undefined };\n const record = body as { message?: unknown; code?: unknown };\n const message =\n typeof record.message === 'string' && record.message !== ''\n ? record.message\n : Array.isArray(record.message) &&\n record.message.length > 0 &&\n record.message.every((each) => typeof each === 'string')\n ? record.message.join('; ')\n : undefined;\n const code = typeof record.code === 'string' && record.code !== '' ? record.code : undefined;\n return { body, message, code };\n}\n\n/** {@link readErrorAnswer} over a `Response` whose status is not 2xx. */\nexport async function readErrorResponse(response: Response): Promise<ErrorAnswer> {\n let text: string | undefined;\n try {\n text = await response.text();\n } catch {\n text = undefined;\n }\n return readErrorAnswer(text);\n}\n\n/**\n * The shape both {@link import('./client.js').AgentHttpError} and `MediaUploadError` share, for\n * code that handles either without an `instanceof` (the two live in separate bundles).\n */\nexport interface AgentRequestError extends Error {\n status: number;\n method: string;\n path: string;\n body: unknown;\n code: string | undefined;\n}\n\n/** Called with every error answer before it is thrown — for app-wide reactions (401, 402, …). */\nexport type HttpErrorListener = (error: AgentRequestError) => void;\n\n/** Tell `listener` about `error`; a listener that throws never replaces the error itself. */\nexport function reportHttpError(\n listener: HttpErrorListener | undefined,\n error: AgentRequestError,\n): void {\n if (listener === undefined) return;\n try {\n listener(error);\n } catch {\n /* the request's own error is what the caller gets */\n }\n}\n","import type {\n AgentCatalogEntry,\n AgentClientConfig,\n ChatQueueState,\n MessageAttachment,\n MessageFeedback,\n ModelCatalogView,\n QuotaReport,\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 QueuedMessageUpdate,\n QueuedSendResult,\n ResumeStreamRequest,\n ThreadPatch,\n UploadAttachmentOptions,\n} from './backend.js';\n\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n type HttpErrorListener,\n readErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from './http-error.js';\n\nexport type { ThreadPatch } from './backend.js';\nexport type { AgentRequestError, HttpErrorListener } from './http-error.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 * and what the server said: `message` is the body's `message` when it sent one (so a hook that\n * shows `error.message` shows the server's words), `code` its machine-readable `code`, and `body`\n * the whole answer, parsed when it is JSON.\n */\nexport class AgentHttpError extends Error implements AgentRequestError {\n /** The answer's body: parsed JSON, else its text; `undefined` when empty. */\n readonly body: unknown;\n /** The body's `code` (`quota_exceeded`, …), when the server sent one. */\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(answer.message ?? `Agent request failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'AgentHttpError';\n this.body = answer.body;\n this.code = answer.code;\n }\n\n /** Read a refused `response`'s body into an error. */\n static async from(response: Response, method: string, path: string): Promise<AgentHttpError> {\n return new AgentHttpError(\n response.status,\n method,\n path,\n response.statusText,\n await readErrorResponse(response),\n );\n }\n}\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 /**\n * Called with every error answer (an {@link AgentHttpError}, or a `MediaUploadError` from a\n * `mediaAttachments()` upload) right before it is thrown — the place for app-wide reactions such\n * as \"401 → sign in again\". The error still reaches the caller. Not called for the `404` a\n * resume answers when nothing is streaming, which is an answer, not a failure.\n */\n onHttpError?: HttpErrorListener;\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.status === 202) {\n // Queued behind a turn already running on the thread: JSON, not a stream.\n return { body: emptyStream(), queued: (await response.json()) as QueuedSendResult };\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'POST', `${this.agentPath()}/chat`);\n }\n return streamResponse(response);\n }\n\n /**\n * `POST <path>/chat` for a message that should wait in the thread's queue — the body carries\n * `mode: 'queue'` (or `'interrupt'`), which the server always answers with `202` JSON.\n */\n enqueueMessage(request: ChatStreamRequest): Promise<QueuedSendResult> {\n return this.request<QueuedSendResult>('POST', '/chat', {\n mode: 'queue',\n ...request.body,\n });\n }\n\n /** `GET <path>/threads/:id/queue` — the thread's waiting messages, and whether it drains. */\n getQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('GET', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `PATCH <path>/queue/:messageId` — change a waiting message's text/attachments, or move it. */\n updateQueuedMessage(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('PATCH', `/queue/${encodeURIComponent(messageId)}`, update);\n }\n\n /** `DELETE <path>/queue/:messageId`. */\n removeQueuedMessage(messageId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/queue/${encodeURIComponent(messageId)}`);\n }\n\n /**\n * `POST <path>/queue/:messageId/interrupt` — run a waiting message now, cancelling the running\n * turn for it. `runId` when nothing was running and it started; else `interrupting`.\n */\n interruptQueuedMessage(\n messageId: string,\n ): Promise<ChatQueueState & { runId?: string; interrupting?: string }> {\n return this.request<ChatQueueState & { runId?: string; interrupting?: string }>(\n 'POST',\n `/queue/${encodeURIComponent(messageId)}/interrupt`,\n );\n }\n\n /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */\n clearQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */\n resumeQueue(threadId: string): Promise<ChatQueueState & { runId?: string }> {\n return this.request<ChatQueueState & { runId?: string }>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/queue/resume`,\n );\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 await this.failure(response, 'GET', `${this.agentPath()}${path}`);\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 /** 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 const error = new AgentHttpError(\n xhr.status,\n 'POST',\n `${this.agentPath()}/attachments`,\n xhr.statusText,\n readErrorAnswer(xhr.responseText),\n );\n reportHttpError(this.options.onHttpError, error);\n reject(error);\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>/config` — attachment limits and upload mode, and which features are on. */\n getConfig(): Promise<AgentClientConfig> {\n return this.request<AgentClientConfig>('GET', '/config');\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 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 * `via` names the surface the answer came through (the server records `'web'` when omitted).\n */\n answerToolCall(input: {\n toolCallId: string;\n answers?: Record<string, string[]>;\n via?: 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; via?: 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 ...(this.options.onHttpError !== undefined ? { onHttpError: this.options.onHttpError } : {}),\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 /** The error for a refused `response`, already reported to `onHttpError`. */\n private async failure(response: Response, method: string, path: string): Promise<AgentHttpError> {\n const error = await AgentHttpError.from(response, method, path);\n reportHttpError(this.options.onHttpError, error);\n return error;\n }\n\n private async handleResponse<T>(response: Response, method: string, path: string): Promise<T> {\n if (!response.ok) {\n throw await this.failure(response, method, path);\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 emptyStream(): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-agent-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/DavideCarvalho/nestjs-agent.git",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"dist"
|
|
84
84
|
],
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@dudousxd/nestjs-agent-core": "0.
|
|
86
|
+
"@dudousxd/nestjs-agent-core": "0.33.0"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@ai-sdk/react": "^4.0.0",
|
|
@@ -178,6 +178,7 @@
|
|
|
178
178
|
},
|
|
179
179
|
"scripts": {
|
|
180
180
|
"build": "tsup",
|
|
181
|
+
"test:dist": "node scripts/verify-dist.mjs",
|
|
181
182
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
182
183
|
"typecheck:specs": "tsc -p tsconfig.spec.json"
|
|
183
184
|
}
|