@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 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.3...HEAD
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
@@ -1,2 +1,3 @@
1
1
  export * from "./platform/index.js";
2
+ export * from "./lib/chatWithAI.js";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -1 +1,2 @@
1
1
  export * from "./platform/index.js";
2
+ export * from "./lib/chatWithAI.js";
@@ -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"}
@@ -1 +1,170 @@
1
- export {};
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
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./platform/index.js";
2
+ export * from "./lib/chatWithAI.js";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./platform/index.js"), exports);
18
+ __exportStar(require("./lib/chatWithAI.js"), exports);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/ai",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Plasius AI functions providing chatbot, text-to-speech, speech-to-text, and AI-generated images and videos",
5
5
  "keywords": [
6
6
  "chatbot",
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from "./platform/index.js";
2
+ export * from "./lib/chatWithAI.js";
@@ -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
+ }