@plasius/ai 1.0.3 → 1.0.4
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/CHANGELOG.md +18 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/chatWithAI.d.ts +41 -1
- package/dist/lib/chatWithAI.d.ts.map +1 -1
- package/dist/lib/chatWithAI.js +170 -1
- package/dist-cjs/index.d.ts +1 -0
- package/dist-cjs/index.d.ts.map +1 -1
- package/dist-cjs/index.js +1 -0
- package/dist-cjs/lib/chatWithAI.d.ts +41 -0
- package/dist-cjs/lib/chatWithAI.d.ts.map +1 -1
- package/dist-cjs/lib/chatWithAI.js +175 -0
- package/docs/adrs/index.md +5 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/lib/chatWithAI.ts +260 -0
package/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,22 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
20
20
|
- **Security**
|
|
21
21
|
- (placeholder)
|
|
22
22
|
|
|
23
|
+
## [1.0.4] - 2026-02-21
|
|
24
|
+
|
|
25
|
+
- **Added**
|
|
26
|
+
- Add a typed chatbot API client (`chatWithAI`, `getChatbotUsage`) for `/ai/chatbot`.
|
|
27
|
+
- Add `ChatbotApiError` with HTTP status/code/usage metadata for auth and quota handling.
|
|
28
|
+
- Add CSRF bootstrap support (GET-first cookie hydration, then `x-csrf-token` on POST).
|
|
29
|
+
|
|
30
|
+
- **Changed**
|
|
31
|
+
- Export chatbot client utilities from the package root.
|
|
32
|
+
|
|
33
|
+
- **Fixed**
|
|
34
|
+
- (placeholder)
|
|
35
|
+
|
|
36
|
+
- **Security**
|
|
37
|
+
- (placeholder)
|
|
38
|
+
|
|
23
39
|
## [1.0.3] - 2026-02-12
|
|
24
40
|
|
|
25
41
|
- **Added**
|
|
@@ -54,7 +70,7 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
54
70
|
|
|
55
71
|
---
|
|
56
72
|
|
|
57
|
-
[Unreleased]: https://github.com/Plasius-LTD/ai/compare/v1.0.
|
|
73
|
+
[Unreleased]: https://github.com/Plasius-LTD/ai/compare/v1.0.4...HEAD
|
|
58
74
|
|
|
59
75
|
## [1.0.0] - 2026-02-11
|
|
60
76
|
|
|
@@ -70,3 +86,4 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
70
86
|
- **Security**
|
|
71
87
|
- (placeholder)
|
|
72
88
|
[1.0.3]: https://github.com/Plasius-LTD/ai/releases/tag/v1.0.3
|
|
89
|
+
[1.0.4]: https://github.com/Plasius-LTD/ai/releases/tag/v1.0.4
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/lib/chatWithAI.d.ts
CHANGED
|
@@ -1,2 +1,42 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ChatRole = "system" | "user" | "assistant";
|
|
2
|
+
export interface ChatMessage {
|
|
3
|
+
role: ChatRole;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatWithAIRequest {
|
|
7
|
+
message: string;
|
|
8
|
+
history?: ChatMessage[];
|
|
9
|
+
systemPrompt?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ChatbotUsage {
|
|
12
|
+
limit: number;
|
|
13
|
+
used: number;
|
|
14
|
+
remaining: number;
|
|
15
|
+
exhausted: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ChatbotResponse {
|
|
18
|
+
reply: string;
|
|
19
|
+
model: string;
|
|
20
|
+
usage: ChatbotUsage;
|
|
21
|
+
}
|
|
22
|
+
export interface ChatbotUsageResponse {
|
|
23
|
+
usage: ChatbotUsage;
|
|
24
|
+
}
|
|
25
|
+
export interface ChatWithAIClientOptions {
|
|
26
|
+
endpoint?: string;
|
|
27
|
+
credentials?: RequestCredentials;
|
|
28
|
+
fetchFn?: typeof fetch;
|
|
29
|
+
authHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
30
|
+
csrfCookieName?: string;
|
|
31
|
+
csrfHeaderName?: string;
|
|
32
|
+
bootstrapCsrf?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class ChatbotApiError extends Error {
|
|
35
|
+
status: number;
|
|
36
|
+
code?: string;
|
|
37
|
+
usage?: ChatbotUsage;
|
|
38
|
+
constructor(status: number, message: string, code?: string, usage?: ChatbotUsage);
|
|
39
|
+
}
|
|
40
|
+
export declare function getChatbotUsage(options?: ChatWithAIClientOptions): Promise<ChatbotUsageResponse>;
|
|
41
|
+
export declare function chatWithAI(request: ChatWithAIRequest, options?: ChatWithAIClientOptions): Promise<ChatbotResponse>;
|
|
2
42
|
//# sourceMappingURL=chatWithAI.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatWithAI.d.ts","sourceRoot":"","sources":["../../src/lib/chatWithAI.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"chatWithAI.d.ts","sourceRoot":"","sources":["../../src/lib/chatWithAI.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,WAAW,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAYD,qBAAa,eAAgB,SAAQ,KAAK;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,YAAY,CAAC;gBAET,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY;CAOjF;AAuID,wBAAsB,eAAe,CACnC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,oBAAoB,CAAC,CAmB/B;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,EAC1B,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,eAAe,CAAC,CAoC1B"}
|
package/dist/lib/chatWithAI.js
CHANGED
|
@@ -1 +1,170 @@
|
|
|
1
|
-
|
|
1
|
+
const DEFAULT_ENDPOINT = "/ai/chatbot";
|
|
2
|
+
const DEFAULT_CSRF_COOKIE_NAME = "csrf-token";
|
|
3
|
+
const DEFAULT_CSRF_HEADER_NAME = "x-csrf-token";
|
|
4
|
+
export class ChatbotApiError extends Error {
|
|
5
|
+
constructor(status, message, code, usage) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ChatbotApiError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.usage = usage;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function resolveFetch(fetchFn) {
|
|
14
|
+
const resolved = fetchFn ?? (typeof fetch !== "undefined" ? fetch : undefined);
|
|
15
|
+
if (!resolved) {
|
|
16
|
+
throw new Error("No fetch implementation available for chatWithAI.");
|
|
17
|
+
}
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
async function resolveAuthHeaders(authHeaders) {
|
|
21
|
+
if (!authHeaders)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof authHeaders === "function") {
|
|
24
|
+
return await authHeaders();
|
|
25
|
+
}
|
|
26
|
+
return authHeaders;
|
|
27
|
+
}
|
|
28
|
+
function readCookie(cookieName) {
|
|
29
|
+
if (typeof document === "undefined" || typeof document.cookie !== "string") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const entries = document.cookie.split(";").map((part) => part.trim());
|
|
33
|
+
const match = entries.find((entry) => entry.startsWith(`${cookieName}=`));
|
|
34
|
+
if (!match)
|
|
35
|
+
return undefined;
|
|
36
|
+
const [, value = ""] = match.split("=");
|
|
37
|
+
try {
|
|
38
|
+
return decodeURIComponent(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function parseResponseBody(response) {
|
|
45
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
46
|
+
if (contentType.includes("application/json")) {
|
|
47
|
+
return await response.json();
|
|
48
|
+
}
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
if (!text)
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function ensureCsrfToken(fetcher, endpoint, options, headers) {
|
|
60
|
+
const csrfCookieName = options.csrfCookieName ?? DEFAULT_CSRF_COOKIE_NAME;
|
|
61
|
+
const existingToken = readCookie(csrfCookieName);
|
|
62
|
+
if (existingToken || options.bootstrapCsrf === false) {
|
|
63
|
+
return existingToken;
|
|
64
|
+
}
|
|
65
|
+
await fetcher(endpoint, {
|
|
66
|
+
method: "GET",
|
|
67
|
+
credentials: options.credentials ?? "include",
|
|
68
|
+
headers,
|
|
69
|
+
});
|
|
70
|
+
return readCookie(csrfCookieName);
|
|
71
|
+
}
|
|
72
|
+
function normalizeError(status, body) {
|
|
73
|
+
const payload = body && typeof body === "object" ? body : undefined;
|
|
74
|
+
const message = payload?.message ||
|
|
75
|
+
(status === 401
|
|
76
|
+
? "You must be signed in to use chatbot."
|
|
77
|
+
: "Chatbot request failed.");
|
|
78
|
+
return new ChatbotApiError(status, message, payload?.error, payload?.usage);
|
|
79
|
+
}
|
|
80
|
+
function buildUsage(payload) {
|
|
81
|
+
if (!payload || typeof payload !== "object")
|
|
82
|
+
return undefined;
|
|
83
|
+
const maybe = payload;
|
|
84
|
+
const limit = maybe.limit;
|
|
85
|
+
const used = maybe.used;
|
|
86
|
+
const remaining = maybe.remaining;
|
|
87
|
+
const exhausted = maybe.exhausted;
|
|
88
|
+
if (typeof limit !== "number" ||
|
|
89
|
+
typeof used !== "number" ||
|
|
90
|
+
typeof remaining !== "number" ||
|
|
91
|
+
typeof exhausted !== "boolean") {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return { limit, used, remaining, exhausted };
|
|
95
|
+
}
|
|
96
|
+
function mapToChatbotResponse(body) {
|
|
97
|
+
if (!body || typeof body !== "object") {
|
|
98
|
+
throw new Error("Invalid chatbot response payload.");
|
|
99
|
+
}
|
|
100
|
+
const payload = body;
|
|
101
|
+
const reply = payload.reply;
|
|
102
|
+
const model = payload.model;
|
|
103
|
+
const usage = buildUsage(payload.usage);
|
|
104
|
+
if (typeof reply !== "string" || typeof model !== "string" || !usage) {
|
|
105
|
+
throw new Error("Invalid chatbot response payload.");
|
|
106
|
+
}
|
|
107
|
+
return { reply, model, usage };
|
|
108
|
+
}
|
|
109
|
+
function mapToChatbotUsageResponse(body) {
|
|
110
|
+
if (!body || typeof body !== "object") {
|
|
111
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
112
|
+
}
|
|
113
|
+
const payload = body;
|
|
114
|
+
const usage = buildUsage(payload.usage);
|
|
115
|
+
if (!usage) {
|
|
116
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
117
|
+
}
|
|
118
|
+
return { usage };
|
|
119
|
+
}
|
|
120
|
+
export async function getChatbotUsage(options = {}) {
|
|
121
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
122
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
123
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
124
|
+
const response = await fetcher(endpoint, {
|
|
125
|
+
method: "GET",
|
|
126
|
+
credentials: options.credentials ?? "include",
|
|
127
|
+
headers: {
|
|
128
|
+
Accept: "application/json",
|
|
129
|
+
...(authHeaders ?? {}),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const body = await parseResponseBody(response);
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw normalizeError(response.status, body);
|
|
135
|
+
}
|
|
136
|
+
return mapToChatbotUsageResponse(body);
|
|
137
|
+
}
|
|
138
|
+
export async function chatWithAI(request, options = {}) {
|
|
139
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
140
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
141
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
142
|
+
const baseHeaders = {
|
|
143
|
+
Accept: "application/json",
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
...(authHeaders ?? {}),
|
|
146
|
+
};
|
|
147
|
+
const csrfToken = await ensureCsrfToken(fetcher, endpoint, options, baseHeaders);
|
|
148
|
+
const csrfHeaderName = options.csrfHeaderName ?? DEFAULT_CSRF_HEADER_NAME;
|
|
149
|
+
const headers = csrfToken
|
|
150
|
+
? {
|
|
151
|
+
...baseHeaders,
|
|
152
|
+
[csrfHeaderName]: csrfToken,
|
|
153
|
+
}
|
|
154
|
+
: baseHeaders;
|
|
155
|
+
const response = await fetcher(endpoint, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
credentials: options.credentials ?? "include",
|
|
158
|
+
headers,
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
message: request.message,
|
|
161
|
+
history: request.history ?? [],
|
|
162
|
+
systemPrompt: request.systemPrompt,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
const body = await parseResponseBody(response);
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw normalizeError(response.status, body);
|
|
168
|
+
}
|
|
169
|
+
return mapToChatbotResponse(body);
|
|
170
|
+
}
|
package/dist-cjs/index.d.ts
CHANGED
package/dist-cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC"}
|
package/dist-cjs/index.js
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
+
export type ChatRole = "system" | "user" | "assistant";
|
|
2
|
+
export interface ChatMessage {
|
|
3
|
+
role: ChatRole;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatWithAIRequest {
|
|
7
|
+
message: string;
|
|
8
|
+
history?: ChatMessage[];
|
|
9
|
+
systemPrompt?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ChatbotUsage {
|
|
12
|
+
limit: number;
|
|
13
|
+
used: number;
|
|
14
|
+
remaining: number;
|
|
15
|
+
exhausted: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ChatbotResponse {
|
|
18
|
+
reply: string;
|
|
19
|
+
model: string;
|
|
20
|
+
usage: ChatbotUsage;
|
|
21
|
+
}
|
|
22
|
+
export interface ChatbotUsageResponse {
|
|
23
|
+
usage: ChatbotUsage;
|
|
24
|
+
}
|
|
25
|
+
export interface ChatWithAIClientOptions {
|
|
26
|
+
endpoint?: string;
|
|
27
|
+
credentials?: RequestCredentials;
|
|
28
|
+
fetchFn?: typeof fetch;
|
|
29
|
+
authHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
30
|
+
csrfCookieName?: string;
|
|
31
|
+
csrfHeaderName?: string;
|
|
32
|
+
bootstrapCsrf?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare class ChatbotApiError extends Error {
|
|
35
|
+
status: number;
|
|
36
|
+
code?: string;
|
|
37
|
+
usage?: ChatbotUsage;
|
|
38
|
+
constructor(status: number, message: string, code?: string, usage?: ChatbotUsage);
|
|
39
|
+
}
|
|
40
|
+
export declare function getChatbotUsage(options?: ChatWithAIClientOptions): Promise<ChatbotUsageResponse>;
|
|
41
|
+
export declare function chatWithAI(request: ChatWithAIRequest, options?: ChatWithAIClientOptions): Promise<ChatbotResponse>;
|
|
1
42
|
//# sourceMappingURL=chatWithAI.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatWithAI.d.ts","sourceRoot":"","sources":["../../src/lib/chatWithAI.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"chatWithAI.d.ts","sourceRoot":"","sources":["../../src/lib/chatWithAI.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,WAAW,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAYD,qBAAa,eAAgB,SAAQ,KAAK;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,YAAY,CAAC;gBAET,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY;CAOjF;AAuID,wBAAsB,eAAe,CACnC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,oBAAoB,CAAC,CAmB/B;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,EAC1B,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,eAAe,CAAC,CAoC1B"}
|
|
@@ -1 +1,176 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatbotApiError = void 0;
|
|
4
|
+
exports.getChatbotUsage = getChatbotUsage;
|
|
5
|
+
exports.chatWithAI = chatWithAI;
|
|
6
|
+
const DEFAULT_ENDPOINT = "/ai/chatbot";
|
|
7
|
+
const DEFAULT_CSRF_COOKIE_NAME = "csrf-token";
|
|
8
|
+
const DEFAULT_CSRF_HEADER_NAME = "x-csrf-token";
|
|
9
|
+
class ChatbotApiError extends Error {
|
|
10
|
+
constructor(status, message, code, usage) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "ChatbotApiError";
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.usage = usage;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ChatbotApiError = ChatbotApiError;
|
|
19
|
+
function resolveFetch(fetchFn) {
|
|
20
|
+
const resolved = fetchFn ?? (typeof fetch !== "undefined" ? fetch : undefined);
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
throw new Error("No fetch implementation available for chatWithAI.");
|
|
23
|
+
}
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
async function resolveAuthHeaders(authHeaders) {
|
|
27
|
+
if (!authHeaders)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof authHeaders === "function") {
|
|
30
|
+
return await authHeaders();
|
|
31
|
+
}
|
|
32
|
+
return authHeaders;
|
|
33
|
+
}
|
|
34
|
+
function readCookie(cookieName) {
|
|
35
|
+
if (typeof document === "undefined" || typeof document.cookie !== "string") {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const entries = document.cookie.split(";").map((part) => part.trim());
|
|
39
|
+
const match = entries.find((entry) => entry.startsWith(`${cookieName}=`));
|
|
40
|
+
if (!match)
|
|
41
|
+
return undefined;
|
|
42
|
+
const [, value = ""] = match.split("=");
|
|
43
|
+
try {
|
|
44
|
+
return decodeURIComponent(value);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function parseResponseBody(response) {
|
|
51
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
52
|
+
if (contentType.includes("application/json")) {
|
|
53
|
+
return await response.json();
|
|
54
|
+
}
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
if (!text)
|
|
57
|
+
return undefined;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function ensureCsrfToken(fetcher, endpoint, options, headers) {
|
|
66
|
+
const csrfCookieName = options.csrfCookieName ?? DEFAULT_CSRF_COOKIE_NAME;
|
|
67
|
+
const existingToken = readCookie(csrfCookieName);
|
|
68
|
+
if (existingToken || options.bootstrapCsrf === false) {
|
|
69
|
+
return existingToken;
|
|
70
|
+
}
|
|
71
|
+
await fetcher(endpoint, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
credentials: options.credentials ?? "include",
|
|
74
|
+
headers,
|
|
75
|
+
});
|
|
76
|
+
return readCookie(csrfCookieName);
|
|
77
|
+
}
|
|
78
|
+
function normalizeError(status, body) {
|
|
79
|
+
const payload = body && typeof body === "object" ? body : undefined;
|
|
80
|
+
const message = payload?.message ||
|
|
81
|
+
(status === 401
|
|
82
|
+
? "You must be signed in to use chatbot."
|
|
83
|
+
: "Chatbot request failed.");
|
|
84
|
+
return new ChatbotApiError(status, message, payload?.error, payload?.usage);
|
|
85
|
+
}
|
|
86
|
+
function buildUsage(payload) {
|
|
87
|
+
if (!payload || typeof payload !== "object")
|
|
88
|
+
return undefined;
|
|
89
|
+
const maybe = payload;
|
|
90
|
+
const limit = maybe.limit;
|
|
91
|
+
const used = maybe.used;
|
|
92
|
+
const remaining = maybe.remaining;
|
|
93
|
+
const exhausted = maybe.exhausted;
|
|
94
|
+
if (typeof limit !== "number" ||
|
|
95
|
+
typeof used !== "number" ||
|
|
96
|
+
typeof remaining !== "number" ||
|
|
97
|
+
typeof exhausted !== "boolean") {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return { limit, used, remaining, exhausted };
|
|
101
|
+
}
|
|
102
|
+
function mapToChatbotResponse(body) {
|
|
103
|
+
if (!body || typeof body !== "object") {
|
|
104
|
+
throw new Error("Invalid chatbot response payload.");
|
|
105
|
+
}
|
|
106
|
+
const payload = body;
|
|
107
|
+
const reply = payload.reply;
|
|
108
|
+
const model = payload.model;
|
|
109
|
+
const usage = buildUsage(payload.usage);
|
|
110
|
+
if (typeof reply !== "string" || typeof model !== "string" || !usage) {
|
|
111
|
+
throw new Error("Invalid chatbot response payload.");
|
|
112
|
+
}
|
|
113
|
+
return { reply, model, usage };
|
|
114
|
+
}
|
|
115
|
+
function mapToChatbotUsageResponse(body) {
|
|
116
|
+
if (!body || typeof body !== "object") {
|
|
117
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
118
|
+
}
|
|
119
|
+
const payload = body;
|
|
120
|
+
const usage = buildUsage(payload.usage);
|
|
121
|
+
if (!usage) {
|
|
122
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
123
|
+
}
|
|
124
|
+
return { usage };
|
|
125
|
+
}
|
|
126
|
+
async function getChatbotUsage(options = {}) {
|
|
127
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
128
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
129
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
130
|
+
const response = await fetcher(endpoint, {
|
|
131
|
+
method: "GET",
|
|
132
|
+
credentials: options.credentials ?? "include",
|
|
133
|
+
headers: {
|
|
134
|
+
Accept: "application/json",
|
|
135
|
+
...(authHeaders ?? {}),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const body = await parseResponseBody(response);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw normalizeError(response.status, body);
|
|
141
|
+
}
|
|
142
|
+
return mapToChatbotUsageResponse(body);
|
|
143
|
+
}
|
|
144
|
+
async function chatWithAI(request, options = {}) {
|
|
145
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
146
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
147
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
148
|
+
const baseHeaders = {
|
|
149
|
+
Accept: "application/json",
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
...(authHeaders ?? {}),
|
|
152
|
+
};
|
|
153
|
+
const csrfToken = await ensureCsrfToken(fetcher, endpoint, options, baseHeaders);
|
|
154
|
+
const csrfHeaderName = options.csrfHeaderName ?? DEFAULT_CSRF_HEADER_NAME;
|
|
155
|
+
const headers = csrfToken
|
|
156
|
+
? {
|
|
157
|
+
...baseHeaders,
|
|
158
|
+
[csrfHeaderName]: csrfToken,
|
|
159
|
+
}
|
|
160
|
+
: baseHeaders;
|
|
161
|
+
const response = await fetcher(endpoint, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
credentials: options.credentials ?? "include",
|
|
164
|
+
headers,
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
message: request.message,
|
|
167
|
+
history: request.history ?? [],
|
|
168
|
+
systemPrompt: request.systemPrompt,
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
const body = await parseResponseBody(response);
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw normalizeError(response.status, body);
|
|
174
|
+
}
|
|
175
|
+
return mapToChatbotResponse(body);
|
|
176
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# ADR Index
|
|
2
|
+
|
|
3
|
+
- [ADR-0001: Standalone @plasius/ai Package Scope](./adr-0001-ai-package-scope.md)
|
|
4
|
+
- [ADR-0002: Public Repository Governance Baseline](./adr-0002-public-repo-governance.md)
|
|
5
|
+
- [ADR-0003: Contracts-First Documentation Baseline](./adr-0003-contracts-first-documentation.md)
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/lib/chatWithAI.ts
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
export type ChatRole = "system" | "user" | "assistant";
|
|
2
|
+
|
|
3
|
+
export interface ChatMessage {
|
|
4
|
+
role: ChatRole;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ChatWithAIRequest {
|
|
9
|
+
message: string;
|
|
10
|
+
history?: ChatMessage[];
|
|
11
|
+
systemPrompt?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChatbotUsage {
|
|
15
|
+
limit: number;
|
|
16
|
+
used: number;
|
|
17
|
+
remaining: number;
|
|
18
|
+
exhausted: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ChatbotResponse {
|
|
22
|
+
reply: string;
|
|
23
|
+
model: string;
|
|
24
|
+
usage: ChatbotUsage;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ChatbotUsageResponse {
|
|
28
|
+
usage: ChatbotUsage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChatWithAIClientOptions {
|
|
32
|
+
endpoint?: string;
|
|
33
|
+
credentials?: RequestCredentials;
|
|
34
|
+
fetchFn?: typeof fetch;
|
|
35
|
+
authHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
36
|
+
csrfCookieName?: string;
|
|
37
|
+
csrfHeaderName?: string;
|
|
38
|
+
bootstrapCsrf?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ErrorBody {
|
|
42
|
+
error?: string;
|
|
43
|
+
message?: string;
|
|
44
|
+
usage?: ChatbotUsage;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_ENDPOINT = "/ai/chatbot";
|
|
48
|
+
const DEFAULT_CSRF_COOKIE_NAME = "csrf-token";
|
|
49
|
+
const DEFAULT_CSRF_HEADER_NAME = "x-csrf-token";
|
|
50
|
+
|
|
51
|
+
export class ChatbotApiError extends Error {
|
|
52
|
+
status: number;
|
|
53
|
+
code?: string;
|
|
54
|
+
usage?: ChatbotUsage;
|
|
55
|
+
|
|
56
|
+
constructor(status: number, message: string, code?: string, usage?: ChatbotUsage) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "ChatbotApiError";
|
|
59
|
+
this.status = status;
|
|
60
|
+
this.code = code;
|
|
61
|
+
this.usage = usage;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveFetch(fetchFn?: typeof fetch): typeof fetch {
|
|
66
|
+
const resolved = fetchFn ?? (typeof fetch !== "undefined" ? fetch : undefined);
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
throw new Error("No fetch implementation available for chatWithAI.");
|
|
69
|
+
}
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveAuthHeaders(
|
|
74
|
+
authHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)
|
|
75
|
+
): Promise<HeadersInit | undefined> {
|
|
76
|
+
if (!authHeaders) return undefined;
|
|
77
|
+
if (typeof authHeaders === "function") {
|
|
78
|
+
return await authHeaders();
|
|
79
|
+
}
|
|
80
|
+
return authHeaders;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readCookie(cookieName: string): string | undefined {
|
|
84
|
+
if (typeof document === "undefined" || typeof document.cookie !== "string") {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const entries = document.cookie.split(";").map((part) => part.trim());
|
|
89
|
+
const match = entries.find((entry) => entry.startsWith(`${cookieName}=`));
|
|
90
|
+
if (!match) return undefined;
|
|
91
|
+
|
|
92
|
+
const [, value = ""] = match.split("=");
|
|
93
|
+
try {
|
|
94
|
+
return decodeURIComponent(value);
|
|
95
|
+
} catch {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function parseResponseBody(response: Response): Promise<unknown> {
|
|
101
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
102
|
+
if (contentType.includes("application/json")) {
|
|
103
|
+
return await response.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const text = await response.text();
|
|
107
|
+
if (!text) return undefined;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(text) as unknown;
|
|
110
|
+
} catch {
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function ensureCsrfToken(
|
|
116
|
+
fetcher: typeof fetch,
|
|
117
|
+
endpoint: string,
|
|
118
|
+
options: ChatWithAIClientOptions,
|
|
119
|
+
headers: HeadersInit
|
|
120
|
+
): Promise<string | undefined> {
|
|
121
|
+
const csrfCookieName = options.csrfCookieName ?? DEFAULT_CSRF_COOKIE_NAME;
|
|
122
|
+
const existingToken = readCookie(csrfCookieName);
|
|
123
|
+
if (existingToken || options.bootstrapCsrf === false) {
|
|
124
|
+
return existingToken;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await fetcher(endpoint, {
|
|
128
|
+
method: "GET",
|
|
129
|
+
credentials: options.credentials ?? "include",
|
|
130
|
+
headers,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return readCookie(csrfCookieName);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeError(status: number, body: unknown): ChatbotApiError {
|
|
137
|
+
const payload = body && typeof body === "object" ? (body as ErrorBody) : undefined;
|
|
138
|
+
const message =
|
|
139
|
+
payload?.message ||
|
|
140
|
+
(status === 401
|
|
141
|
+
? "You must be signed in to use chatbot."
|
|
142
|
+
: "Chatbot request failed.");
|
|
143
|
+
|
|
144
|
+
return new ChatbotApiError(status, message, payload?.error, payload?.usage);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildUsage(payload: unknown): ChatbotUsage | undefined {
|
|
148
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
149
|
+
const maybe = payload as Record<string, unknown>;
|
|
150
|
+
const limit = maybe.limit;
|
|
151
|
+
const used = maybe.used;
|
|
152
|
+
const remaining = maybe.remaining;
|
|
153
|
+
const exhausted = maybe.exhausted;
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
typeof limit !== "number" ||
|
|
157
|
+
typeof used !== "number" ||
|
|
158
|
+
typeof remaining !== "number" ||
|
|
159
|
+
typeof exhausted !== "boolean"
|
|
160
|
+
) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { limit, used, remaining, exhausted };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function mapToChatbotResponse(body: unknown): ChatbotResponse {
|
|
168
|
+
if (!body || typeof body !== "object") {
|
|
169
|
+
throw new Error("Invalid chatbot response payload.");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const payload = body as Record<string, unknown>;
|
|
173
|
+
const reply = payload.reply;
|
|
174
|
+
const model = payload.model;
|
|
175
|
+
const usage = buildUsage(payload.usage);
|
|
176
|
+
|
|
177
|
+
if (typeof reply !== "string" || typeof model !== "string" || !usage) {
|
|
178
|
+
throw new Error("Invalid chatbot response payload.");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { reply, model, usage };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function mapToChatbotUsageResponse(body: unknown): ChatbotUsageResponse {
|
|
185
|
+
if (!body || typeof body !== "object") {
|
|
186
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const payload = body as Record<string, unknown>;
|
|
190
|
+
const usage = buildUsage(payload.usage);
|
|
191
|
+
if (!usage) {
|
|
192
|
+
throw new Error("Invalid chatbot usage payload.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { usage };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function getChatbotUsage(
|
|
199
|
+
options: ChatWithAIClientOptions = {}
|
|
200
|
+
): Promise<ChatbotUsageResponse> {
|
|
201
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
202
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
203
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
204
|
+
const response = await fetcher(endpoint, {
|
|
205
|
+
method: "GET",
|
|
206
|
+
credentials: options.credentials ?? "include",
|
|
207
|
+
headers: {
|
|
208
|
+
Accept: "application/json",
|
|
209
|
+
...(authHeaders ?? {}),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const body = await parseResponseBody(response);
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
throw normalizeError(response.status, body);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return mapToChatbotUsageResponse(body);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function chatWithAI(
|
|
222
|
+
request: ChatWithAIRequest,
|
|
223
|
+
options: ChatWithAIClientOptions = {}
|
|
224
|
+
): Promise<ChatbotResponse> {
|
|
225
|
+
const fetcher = resolveFetch(options.fetchFn);
|
|
226
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
227
|
+
const authHeaders = await resolveAuthHeaders(options.authHeaders);
|
|
228
|
+
const baseHeaders: HeadersInit = {
|
|
229
|
+
Accept: "application/json",
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
...(authHeaders ?? {}),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const csrfToken = await ensureCsrfToken(fetcher, endpoint, options, baseHeaders);
|
|
235
|
+
const csrfHeaderName = options.csrfHeaderName ?? DEFAULT_CSRF_HEADER_NAME;
|
|
236
|
+
const headers = csrfToken
|
|
237
|
+
? {
|
|
238
|
+
...baseHeaders,
|
|
239
|
+
[csrfHeaderName]: csrfToken,
|
|
240
|
+
}
|
|
241
|
+
: baseHeaders;
|
|
242
|
+
|
|
243
|
+
const response = await fetcher(endpoint, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
credentials: options.credentials ?? "include",
|
|
246
|
+
headers,
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
message: request.message,
|
|
249
|
+
history: request.history ?? [],
|
|
250
|
+
systemPrompt: request.systemPrompt,
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const body = await parseResponseBody(response);
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw normalizeError(response.status, body);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return mapToChatbotResponse(body);
|
|
260
|
+
}
|