@plasius/ai 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,8 +12,41 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
12
12
  - (placeholder)
13
13
 
14
14
  - **Changed**
15
+ - Hardened GitHub CD publish flow to publish only after successful install, test, and build, then push tags/releases post-publish.
16
+ - Standardized npm publish path on workflow-dispatched `.github/workflows/cd.yml` using provenance and production environment secrets.
17
+ - Replaced `audit:deps` from `depcheck` to `npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true` to avoid deprecated dependency-chain risk.
18
+
19
+ - **Fixed**
15
20
  - (placeholder)
16
21
 
22
+ - **Security**
23
+ - Removed `depcheck` (and its `multimatch`/`minimatch` chain) from devDependencies to resolve reported high-severity audit findings.
24
+
25
+ ## [1.0.4] - 2026-02-21
26
+
27
+ - **Added**
28
+ - Added `npm run demo:run` for one-command local package/demo verification.
29
+
30
+ - **Changed**
31
+ - Aligned OpenAI requirement to `^5.23.2` to match current `plasius-ltd-site` resolved baseline.
32
+ - Updated React Router and toolchain dependency minimums to current `plasius-ltd-site` requirements.
33
+
34
+ - **Fixed**
35
+ - Updated demo docs to run via the package script instead of manual multi-step commands.
36
+
37
+ - **Security**
38
+ - (placeholder)
39
+
40
+ ## [1.0.4] - 2026-02-21
41
+
42
+ - **Added**
43
+ - Add a typed chatbot API client (`chatWithAI`, `getChatbotUsage`) for `/ai/chatbot`.
44
+ - Add `ChatbotApiError` with HTTP status/code/usage metadata for auth and quota handling.
45
+ - Add CSRF bootstrap support (GET-first cookie hydration, then `x-csrf-token` on POST).
46
+
47
+ - **Changed**
48
+ - Export chatbot client utilities from the package root.
49
+
17
50
  - **Fixed**
18
51
  - (placeholder)
19
52
 
@@ -54,7 +87,8 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
54
87
 
55
88
  ---
56
89
 
57
- [Unreleased]: https://github.com/Plasius-LTD/ai/compare/v1.0.3...HEAD
90
+ [Unreleased]: https://github.com/Plasius-LTD/ai/compare/v1.0.4...HEAD
91
+ [1.0.4]: https://github.com/Plasius-LTD/ai/releases/tag/v1.0.4
58
92
 
59
93
  ## [1.0.0] - 2026-02-11
60
94
 
@@ -70,3 +104,4 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
70
104
  - **Security**
71
105
  - (placeholder)
72
106
  [1.0.3]: https://github.com/Plasius-LTD/ai/releases/tag/v1.0.3
107
+ [1.0.4]: https://github.com/Plasius-LTD/ai/releases/tag/v1.0.4
package/README.md CHANGED
@@ -119,8 +119,23 @@ npm install
119
119
  npm run build
120
120
  npm test
121
121
  npm run test:coverage
122
+ npm run demo:run
122
123
  ```
123
124
 
125
+ ## Demo Sanity Check
126
+
127
+ ```bash
128
+ npm run demo:run
129
+ ```
130
+
131
+ ## Publishing
132
+
133
+ This package is published via GitHub CD only.
134
+
135
+ 1. Configure repository environment `production` with secret `NPM_TOKEN`.
136
+ 2. Run `.github/workflows/cd.yml` via **Actions -> CD (Publish to npm) -> Run workflow**.
137
+ 3. Select the version bump (`patch`, `minor`, `major`, or `none`) and optional pre-release id.
138
+
124
139
  ## Build Outputs
125
140
 
126
141
  - ESM: `dist/`
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.1.0",
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",
@@ -24,13 +24,14 @@
24
24
  "reset:clean": "rm -rf node_modules package-lock.json && npm run clean",
25
25
  "audit:ts": "tsc --noEmit --pretty",
26
26
  "audit:eslint": "eslint \"{src,apps,packages}/**/*.{ts,tsx}\" --max-warnings=0 --ext .ts,.tsx",
27
- "audit:deps": "depcheck --skip-missing=true",
27
+ "audit:deps": "npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true",
28
28
  "audit:npm": "npm audit --audit-level=moderate || true",
29
29
  "audit:test": "vitest run --coverage",
30
30
  "audit:all": "npm-run-all -l audit:ts audit:eslint audit:deps audit:npm audit:test",
31
31
  "build:cjs": "tsc -p tsconfig.json --module commonjs --moduleResolution node --outDir dist-cjs --tsBuildInfoFile dist-cjs/tsconfig.tsbuildinfo",
32
32
  "lint": "eslint .",
33
- "prepare": "npm run build"
33
+ "prepare": "npm run build",
34
+ "demo:run": "npm run build && node demo/example.mjs"
34
35
  },
35
36
  "author": "Plasius LTD <development@plasius.co.uk>",
36
37
  "license": "MIT",
@@ -41,29 +42,28 @@
41
42
  "@plasius/schema": "^1.0.0"
42
43
  },
43
44
  "peerDependencies": {
44
- "openai": "^5.19.1",
45
+ "openai": "^5.23.2",
45
46
  "react": "^19.1.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@testing-library/react": "^16.3.0",
49
50
  "@types/node": "^24.3.1",
50
- "@types/react": "^19.1.8",
51
+ "@types/react": "^19.2.14",
51
52
  "@types/uuid": "^10.0.0",
52
- "@typescript-eslint/eslint-plugin": "^8.38.0",
53
- "@typescript-eslint/parser": "^8.38.0",
53
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
54
+ "@typescript-eslint/parser": "^8.55.0",
54
55
  "@vitest/coverage-v8": "^3.2.4",
55
56
  "ajv": "^6.12.6",
56
- "depcheck": "^1.4.7",
57
- "eslint": "^9.33.0",
57
+ "eslint": "^9.39.2",
58
58
  "npm-run-all": "^4.1.5",
59
- "openai": "^5.19.1",
60
- "react": "19.1.0",
61
- "react-dom": "19.1.0",
62
- "react-router-dom": "^7.6.2",
63
- "tsx": "^4.20.3",
64
- "typescript": "^5.8.3",
59
+ "openai": "^5.23.2",
60
+ "react": "19.2.4",
61
+ "react-dom": "19.2.4",
62
+ "react-router-dom": "^7.13.0",
63
+ "tsx": "^4.21.0",
64
+ "typescript": "^5.9.3",
65
65
  "vitest": "^3.2.4",
66
- "zod": "^4.1.5"
66
+ "zod": "^4.1.12"
67
67
  },
68
68
  "sideEffects": [
69
69
  "*.css"
@@ -113,5 +113,6 @@
113
113
  ],
114
114
  "engines": {
115
115
  "node": ">=22.12"
116
- }
116
+ },
117
+ "packageManager": "npm@11.4.2"
117
118
  }
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
+ }