@easy-cms/plugin-ai 0.0.0-stage → 1.33.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kanawoot K.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,32 @@
1
- # Temporary Holding Version
1
+ # @easy-cms/plugin-ai
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AI for Easy CMS: connect Anthropic (Claude), OpenAI (GPT), Google Gemini or an OpenAI-compatible server (Ollama, OpenRouter) on the Integrations page, for the admin's AI features. Keys stay on the server; each user has a daily limit, and usage shows per connection, user and feature. Part of [Easy CMS](https://github.com/maritonx/easy-cms), the embedded, code-first headless CMS for Nuxt and Next.js.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @easy-cms/plugin-ai
9
+ ```
10
+
11
+ Or `pnpm add`, `yarn add` or `bun add`.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { aiPlugin } from '@easy-cms/plugin-ai'
17
+
18
+ export default defineConfig({
19
+ // …
20
+ plugins: [aiPlugin({ limits: { perUserPerDay: 100 } })],
21
+ })
22
+ ```
23
+
24
+ Then create a migration (`easy-cms migrate:create ai`), and in the admin go to **Settings → Integrations → AI**: accept the provider's policy, paste the API key (or set `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` or `GEMINI_API_KEY`), and choose a model.
25
+
26
+ On the server, features ask `cms.ai.generate({ feature, prompt })`.
27
+
28
+ ## Links
29
+
30
+ [AI](https://easycms.dev/docs/ai) · [Documentation](https://easycms.dev/docs) ([ภาษาไทย](https://easycms.dev/th/docs)) · [GitHub](https://github.com/maritonx/easy-cms)
31
+
32
+ MIT License
@@ -0,0 +1,46 @@
1
+ import * as _easy_cms_core from '@easy-cms/core';
2
+ import { IntegrationConfig, AiProvider } from '@easy-cms/core';
3
+
4
+ type Values = Readonly<Record<string, string>>;
5
+ type Choice = {
6
+ value: string;
7
+ label: string;
8
+ };
9
+ /** A provider: its card on the Integrations page, and how it answers. */
10
+ interface AiProviderDefinition {
11
+ readonly integration: IntegrationConfig;
12
+ readonly generate: AiProvider['generate'];
13
+ /** The models it offers, for the model's Find button. */
14
+ readonly models: (values: Values) => Promise<Choice[]>;
15
+ readonly defaultModel?: string;
16
+ }
17
+ declare const anthropic: AiProviderDefinition;
18
+ declare const openai: AiProviderDefinition;
19
+ declare const openaiCompatible: AiProviderDefinition;
20
+ declare const gemini: AiProviderDefinition;
21
+ declare const PROVIDERS: {
22
+ readonly anthropic: AiProviderDefinition;
23
+ readonly openai: AiProviderDefinition;
24
+ readonly gemini: AiProviderDefinition;
25
+ readonly 'openai-compatible': AiProviderDefinition;
26
+ };
27
+ type ProviderId = keyof typeof PROVIDERS;
28
+
29
+ interface AiPluginOptions {
30
+ /** The providers offered on the Integrations page. Default: all four. */
31
+ readonly providers?: readonly ProviderId[];
32
+ readonly limits?: {
33
+ /** Requests a user makes a day. Admins can change it. Default 100. */
34
+ readonly perUserPerDay?: number;
35
+ /** Per role, e.g. `{ editor: 50 }`; 0 keeps a role from using AI. */
36
+ readonly roles?: Readonly<Record<string, number>>;
37
+ };
38
+ }
39
+ /**
40
+ * AI for the admin's features: admins connect Claude, GPT, Gemini or an OpenAI-compatible server
41
+ * in Settings → Integrations (AI), and features ask `cms.ai.generate()`.
42
+ */
43
+ declare function aiPlugin(options?: AiPluginOptions): _easy_cms_core.TypedPlugin<Record<never, never>>;
44
+
45
+ export { PROVIDERS, aiPlugin, anthropic, gemini, openai, openaiCompatible };
46
+ export type { AiPluginOptions, AiProviderDefinition, ProviderId };
package/dist/index.js ADDED
@@ -0,0 +1,371 @@
1
+ // src/plugin.ts
2
+ import { definePlugin } from "@easy-cms/core";
3
+
4
+ // src/info.ts
5
+ var INFO = {
6
+ name: "@easy-cms/plugin-ai",
7
+ // The plugin API it is written for (`PLUGIN_API_VERSION` of @easy-cms/core).
8
+ apiVersion: 1,
9
+ ...true ? { version: "1.33.0" } : {}
10
+ };
11
+
12
+ // src/providers.ts
13
+ import {
14
+ AiProviderError
15
+ } from "@easy-cms/core";
16
+
17
+ // ../../node_modules/.pnpm/simple-icons@16.36.0/node_modules/simple-icons/index.mjs
18
+ var c = '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>';
19
+ var t = '</title><path d="';
20
+ var a = '"/></svg>';
21
+ var siAnthropic = { title: "Anthropic", slug: "anthropic", get svg() {
22
+ return c + "Anthropic" + t + this.path + a;
23
+ }, path: "M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z", source: "https://www.anthropic.com", hex: "191919" };
24
+ var siGooglegemini = { title: "Google Gemini", slug: "googlegemini", get svg() {
25
+ return c + "Google Gemini" + t + this.path + a;
26
+ }, path: "M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81", source: "https://gemini.google.com", hex: "8E75B2" };
27
+
28
+ // src/logos.ts
29
+ var simple = (icon) => ({
30
+ paths: [{ d: icon.path }],
31
+ color: `#${icon.hex}`
32
+ });
33
+ var anthropicLogo = simple(siAnthropic);
34
+ var geminiLogo = simple(siGooglegemini);
35
+ var openaiLogo = {
36
+ viewBox: "0 0 256 260",
37
+ paths: [
38
+ {
39
+ d: "M239.184 106.203a64.72 64.72 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.72 64.72 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.67 64.67 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.77 64.77 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483m-97.56 136.338a48.4 48.4 0 0 1-31.105-11.255l1.535-.87l51.67-29.825a8.6 8.6 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601M37.158 197.93a48.35 48.35 0 0 1-5.781-32.589l1.534.921l51.722 29.826a8.34 8.34 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803M23.549 85.38a48.5 48.5 0 0 1 25.58-21.333v61.39a8.29 8.29 0 0 0 4.195 7.316l62.874 36.272l-21.845 12.636a.82.82 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405zm179.466 41.695l-63.08-36.63L161.73 77.86a.82.82 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.54 8.54 0 0 0-4.4-7.213m21.742-32.69l-1.535-.922l-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.72.72 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391zM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87l-51.67 29.825a8.6 8.6 0 0 0-4.246 7.367zm11.868-25.58L128.067 97.3l28.188 16.218v32.434l-28.086 16.218l-28.188-16.218z"
40
+ }
41
+ ],
42
+ color: "#000000"
43
+ };
44
+
45
+ // src/providers.ts
46
+ var trim = (url) => url.replace(/\/+$/, "");
47
+ async function request(url, init) {
48
+ let response;
49
+ try {
50
+ response = await fetch(url, {
51
+ ...init,
52
+ signal: init.signal ?? AbortSignal.timeout(3e4)
53
+ });
54
+ } catch (error) {
55
+ if (error.name === "TimeoutError") throw error;
56
+ throw new AiProviderError(`The AI provider could not be reached: ${error.message}`);
57
+ }
58
+ const body = await response.json().catch(() => ({}));
59
+ if (!response.ok) {
60
+ const error = body.error;
61
+ const message = typeof error === "string" ? error : error?.message;
62
+ throw new AiProviderError(message ?? `HTTP ${response.status}`, response.status);
63
+ }
64
+ return body;
65
+ }
66
+ function systemOf(req) {
67
+ const json = req.json ? `Answer with JSON only, no other text, matching this JSON schema: ${JSON.stringify(req.json)}` : void 0;
68
+ return [req.system, json].filter(Boolean).join("\n\n") || void 0;
69
+ }
70
+ var keyField = (label, required = true) => ({
71
+ name: "apiKey",
72
+ label: { en: "API key", th: "API key" },
73
+ description: { en: `From ${label}.`, th: `\u0E08\u0E32\u0E01 ${label}` },
74
+ secret: true,
75
+ required
76
+ });
77
+ var modelField = (defaultModel, models) => ({
78
+ name: "model",
79
+ label: { en: "Model", th: "Model" },
80
+ ...defaultModel ? {
81
+ placeholder: defaultModel,
82
+ description: {
83
+ en: `Empty: ${defaultModel}. Find lists the models your key can use.`,
84
+ th: `\u0E16\u0E49\u0E32\u0E27\u0E48\u0E32\u0E07: ${defaultModel} \u0E1B\u0E38\u0E48\u0E21\u0E04\u0E49\u0E19\u0E2B\u0E32\u0E41\u0E2A\u0E14\u0E07 model \u0E17\u0E35\u0E48 key \u0E19\u0E35\u0E49\u0E43\u0E0A\u0E49\u0E44\u0E14\u0E49`
85
+ }
86
+ } : {
87
+ required: true,
88
+ description: { en: "Find lists the models offered.", th: "\u0E1B\u0E38\u0E48\u0E21\u0E04\u0E49\u0E19\u0E2B\u0E32\u0E41\u0E2A\u0E14\u0E07 model \u0E17\u0E35\u0E48\u0E21\u0E35" }
89
+ },
90
+ lookup: (values) => models(values)
91
+ });
92
+ var consent = (provider, policy) => ({
93
+ text: {
94
+ en: `Text and images used with AI features are sent to ${provider}, which handles them under its own policy. Keys stay on this server.`,
95
+ th: `\u0E02\u0E49\u0E2D\u0E04\u0E27\u0E32\u0E21\u0E41\u0E25\u0E30\u0E23\u0E39\u0E1B\u0E17\u0E35\u0E48\u0E43\u0E0A\u0E49\u0E01\u0E31\u0E1A\u0E1F\u0E35\u0E40\u0E08\u0E2D\u0E23\u0E4C AI \u0E08\u0E30\u0E16\u0E39\u0E01\u0E2A\u0E48\u0E07\u0E44\u0E1B\u0E17\u0E35\u0E48 ${provider} \u0E0B\u0E36\u0E48\u0E07\u0E08\u0E31\u0E14\u0E01\u0E32\u0E23\u0E15\u0E32\u0E21\u0E19\u0E42\u0E22\u0E1A\u0E32\u0E22\u0E02\u0E2D\u0E07\u0E15\u0E31\u0E27\u0E40\u0E2D\u0E07 key \u0E2D\u0E22\u0E39\u0E48\u0E1A\u0E19\u0E40\u0E0B\u0E34\u0E23\u0E4C\u0E1F\u0E40\u0E27\u0E2D\u0E23\u0E4C\u0E19\u0E35\u0E49\u0E40\u0E17\u0E48\u0E32\u0E19\u0E31\u0E49\u0E19`
96
+ },
97
+ policy
98
+ });
99
+ var anthropicURL = () => trim(process.env.ANTHROPIC_API_URL ?? "https://api.anthropic.com");
100
+ var anthropicHeaders = (values) => ({
101
+ "content-type": "application/json",
102
+ "x-api-key": values.apiKey ?? "",
103
+ "anthropic-version": "2023-06-01"
104
+ });
105
+ var anthropicModels = async (values) => {
106
+ const body = await request(`${anthropicURL()}/v1/models?limit=100`, {
107
+ headers: anthropicHeaders(values)
108
+ });
109
+ return (body.data ?? []).map((m) => ({
110
+ value: m.id,
111
+ label: m.display_name ? `${m.display_name} (${m.id})` : m.id
112
+ }));
113
+ };
114
+ var anthropic = {
115
+ defaultModel: "claude-sonnet-5-5",
116
+ integration: {
117
+ id: "anthropic",
118
+ label: "Anthropic (Claude)",
119
+ category: "ai",
120
+ icon: anthropicLogo,
121
+ description: { en: "Claude models", th: "model Claude" },
122
+ plugin: "@easy-cms/plugin-ai",
123
+ docs: "https://easycms.dev/docs/ai",
124
+ kind: "fields",
125
+ fields: [keyField("console.anthropic.com"), modelField("claude-sonnet-5-5", anthropicModels)],
126
+ env: { apiKey: "ANTHROPIC_API_KEY" },
127
+ consent: consent("Anthropic", "https://www.anthropic.com/legal/privacy")
128
+ },
129
+ models: anthropicModels,
130
+ async generate(values, req) {
131
+ const system = systemOf(req);
132
+ const body = await request(`${anthropicURL()}/v1/messages`, {
133
+ method: "POST",
134
+ headers: anthropicHeaders(values),
135
+ signal: req.signal,
136
+ body: JSON.stringify({
137
+ model: values.model || "claude-sonnet-5-5",
138
+ max_tokens: req.maxTokens,
139
+ ...system ? { system } : {},
140
+ messages: [
141
+ {
142
+ role: "user",
143
+ content: [
144
+ ...req.images.map((i) => ({
145
+ type: "image",
146
+ source: { type: "base64", media_type: i.mediaType, data: i.data }
147
+ })),
148
+ { type: "text", text: req.prompt }
149
+ ]
150
+ }
151
+ ]
152
+ })
153
+ });
154
+ const content = body.content ?? [];
155
+ const usage = body.usage ?? {};
156
+ return {
157
+ text: content.filter((c2) => c2.type === "text").map((c2) => c2.text ?? "").join(""),
158
+ usage: { input: usage.input_tokens ?? 0, output: usage.output_tokens ?? 0 }
159
+ };
160
+ }
161
+ };
162
+ function openAiCompatible(options) {
163
+ const headers = (values) => ({
164
+ "content-type": "application/json",
165
+ ...values.apiKey ? { authorization: `Bearer ${values.apiKey}` } : {}
166
+ });
167
+ const models = async (values) => {
168
+ const body = await request(`${options.base(values)}/models`, { headers: headers(values) });
169
+ return (body.data ?? []).map((m) => ({ value: m.id, label: m.id })).sort((a2, b) => a2.value.localeCompare(b.value));
170
+ };
171
+ return {
172
+ ...options.defaultModel ? { defaultModel: options.defaultModel } : {},
173
+ integration: {
174
+ id: options.id,
175
+ label: options.label,
176
+ category: "ai",
177
+ icon: options.icon,
178
+ description: options.description,
179
+ plugin: "@easy-cms/plugin-ai",
180
+ docs: "https://easycms.dev/docs/ai",
181
+ kind: "fields",
182
+ fields: options.fields(models),
183
+ ...options.env ? { env: options.env } : {},
184
+ ...options.consent ? { consent: options.consent } : {}
185
+ },
186
+ models,
187
+ async generate(values, req) {
188
+ const system = systemOf(req);
189
+ const text = { type: "text", text: req.prompt };
190
+ const images = req.images.map((i) => ({
191
+ type: "image_url",
192
+ image_url: { url: `data:${i.mediaType};base64,${i.data}` }
193
+ }));
194
+ const body = await request(`${options.base(values)}/chat/completions`, {
195
+ method: "POST",
196
+ headers: headers(values),
197
+ signal: req.signal,
198
+ body: JSON.stringify({
199
+ model: values.model || options.defaultModel,
200
+ [options.official ? "max_completion_tokens" : "max_tokens"]: req.maxTokens,
201
+ messages: [
202
+ ...system ? [{ role: "system", content: system }] : [],
203
+ // Text alone as a string: some compatible servers take nothing else.
204
+ { role: "user", content: images.length > 0 ? [text, ...images] : req.prompt }
205
+ ],
206
+ ...req.json ? { response_format: { type: "json_object" } } : {}
207
+ })
208
+ });
209
+ const choices = body.choices ?? [];
210
+ const usage = body.usage ?? {};
211
+ return {
212
+ text: choices[0]?.message?.content ?? "",
213
+ usage: { input: usage.prompt_tokens ?? 0, output: usage.completion_tokens ?? 0 }
214
+ };
215
+ }
216
+ };
217
+ }
218
+ var openai = openAiCompatible({
219
+ id: "openai",
220
+ label: "OpenAI (GPT)",
221
+ icon: openaiLogo,
222
+ description: { en: "GPT models", th: "model GPT" },
223
+ base: () => trim(process.env.OPENAI_API_URL ?? "https://api.openai.com/v1"),
224
+ defaultModel: "gpt-5-mini",
225
+ official: true,
226
+ fields: (models) => [keyField("platform.openai.com"), modelField("gpt-5-mini", models)],
227
+ env: { apiKey: "OPENAI_API_KEY" },
228
+ consent: consent("OpenAI", "https://openai.com/policies/privacy-policy")
229
+ });
230
+ var openaiCompatible = openAiCompatible({
231
+ id: "openai-compatible",
232
+ label: "OpenAI-compatible",
233
+ // A server of your own: no one brand's logo.
234
+ icon: "globe",
235
+ description: {
236
+ en: "Ollama, OpenRouter or another server with an OpenAI-style API",
237
+ th: "Ollama, OpenRouter \u0E2B\u0E23\u0E37\u0E2D\u0E40\u0E0B\u0E34\u0E23\u0E4C\u0E1F\u0E40\u0E27\u0E2D\u0E23\u0E4C\u0E2D\u0E37\u0E48\u0E19\u0E17\u0E35\u0E48\u0E21\u0E35 API \u0E41\u0E1A\u0E1A OpenAI"
238
+ },
239
+ base: (values) => trim(values.baseUrl ?? ""),
240
+ official: false,
241
+ fields: (models) => [
242
+ {
243
+ name: "baseUrl",
244
+ label: { en: "Base URL", th: "Base URL" },
245
+ type: "url",
246
+ placeholder: "http://localhost:11434/v1",
247
+ description: {
248
+ en: "Up to /v1, e.g. https://openrouter.ai/api/v1",
249
+ th: "\u0E16\u0E36\u0E07 /v1 \u0E40\u0E0A\u0E48\u0E19 https://openrouter.ai/api/v1"
250
+ },
251
+ required: true
252
+ },
253
+ keyField("the provider, if it needs one", false),
254
+ modelField(void 0, models)
255
+ ],
256
+ consent: {
257
+ text: {
258
+ en: "Text and images used with AI features are sent to the server at this address, which handles them under its own policy.",
259
+ th: "\u0E02\u0E49\u0E2D\u0E04\u0E27\u0E32\u0E21\u0E41\u0E25\u0E30\u0E23\u0E39\u0E1B\u0E17\u0E35\u0E48\u0E43\u0E0A\u0E49\u0E01\u0E31\u0E1A\u0E1F\u0E35\u0E40\u0E08\u0E2D\u0E23\u0E4C AI \u0E08\u0E30\u0E16\u0E39\u0E01\u0E2A\u0E48\u0E07\u0E44\u0E1B\u0E17\u0E35\u0E48\u0E40\u0E0B\u0E34\u0E23\u0E4C\u0E1F\u0E40\u0E27\u0E2D\u0E23\u0E4C\u0E15\u0E32\u0E21\u0E17\u0E35\u0E48\u0E2D\u0E22\u0E39\u0E48\u0E19\u0E35\u0E49 \u0E0B\u0E36\u0E48\u0E07\u0E08\u0E31\u0E14\u0E01\u0E32\u0E23\u0E15\u0E32\u0E21\u0E19\u0E42\u0E22\u0E1A\u0E32\u0E22\u0E02\u0E2D\u0E07\u0E15\u0E31\u0E27\u0E40\u0E2D\u0E07"
260
+ }
261
+ }
262
+ });
263
+ var geminiURL = () => trim(process.env.GEMINI_API_URL ?? "https://generativelanguage.googleapis.com");
264
+ var geminiHeaders = (values) => ({
265
+ "content-type": "application/json",
266
+ "x-goog-api-key": values.apiKey ?? ""
267
+ });
268
+ var geminiModels = async (values) => {
269
+ const body = await request(`${geminiURL()}/v1beta/models?pageSize=1000`, {
270
+ headers: geminiHeaders(values)
271
+ });
272
+ return (body.models ?? []).filter((m) => m.supportedGenerationMethods?.includes("generateContent") ?? true).map((m) => {
273
+ const id = m.name.replace(/^models\//, "");
274
+ return { value: id, label: m.displayName ? `${m.displayName} (${id})` : id };
275
+ });
276
+ };
277
+ var gemini = {
278
+ defaultModel: "gemini-flash-latest",
279
+ integration: {
280
+ id: "gemini",
281
+ label: "Google Gemini",
282
+ category: "ai",
283
+ icon: geminiLogo,
284
+ description: { en: "Gemini models", th: "model Gemini" },
285
+ plugin: "@easy-cms/plugin-ai",
286
+ docs: "https://easycms.dev/docs/ai",
287
+ kind: "fields",
288
+ fields: [keyField("aistudio.google.com"), modelField("gemini-flash-latest", geminiModels)],
289
+ env: { apiKey: "GEMINI_API_KEY" },
290
+ consent: consent("Google", "https://ai.google.dev/gemini-api/terms")
291
+ },
292
+ models: geminiModels,
293
+ async generate(values, req) {
294
+ const system = systemOf(req);
295
+ const model = values.model || "gemini-flash-latest";
296
+ const body = await request(
297
+ `${geminiURL()}/v1beta/models/${encodeURIComponent(model)}:generateContent`,
298
+ {
299
+ method: "POST",
300
+ headers: geminiHeaders(values),
301
+ signal: req.signal,
302
+ body: JSON.stringify({
303
+ ...system ? { systemInstruction: { parts: [{ text: system }] } } : {},
304
+ contents: [
305
+ {
306
+ role: "user",
307
+ parts: [
308
+ ...req.images.map((i) => ({ inlineData: { mimeType: i.mediaType, data: i.data } })),
309
+ { text: req.prompt }
310
+ ]
311
+ }
312
+ ],
313
+ generationConfig: {
314
+ maxOutputTokens: req.maxTokens,
315
+ ...req.json ? { responseMimeType: "application/json" } : {}
316
+ }
317
+ })
318
+ }
319
+ );
320
+ const candidates = body.candidates ?? [];
321
+ const usage = body.usageMetadata ?? {};
322
+ return {
323
+ text: (candidates[0]?.content?.parts ?? []).map((p) => p.text ?? "").join(""),
324
+ usage: { input: usage.promptTokenCount ?? 0, output: usage.candidatesTokenCount ?? 0 }
325
+ };
326
+ }
327
+ };
328
+ var PROVIDERS = {
329
+ anthropic,
330
+ openai,
331
+ gemini,
332
+ "openai-compatible": openaiCompatible
333
+ };
334
+
335
+ // src/plugin.ts
336
+ function aiPlugin(options = {}) {
337
+ const ids = options.providers ?? Object.keys(PROVIDERS);
338
+ const unknown = ids.filter((id) => !(id in PROVIDERS));
339
+ if (unknown.length > 0)
340
+ throw new Error(
341
+ `aiPlugin: unknown provider ${unknown.map((id) => `"${id}"`).join(", ")}; use ${Object.keys(PROVIDERS).join(", ")}`
342
+ );
343
+ const chosen = ids.map((id) => PROVIDERS[id]);
344
+ return definePlugin((config) => {
345
+ if (config.ai) throw new Error("aiPlugin: `ai` is set already; add the plugin once");
346
+ const integrations = chosen.map(({ integration, defaultModel }) => ({
347
+ ...integration,
348
+ async test(values, { cms, name }) {
349
+ if (!cms.ai) return { ok: false, message: "AI is not set up" };
350
+ const result = await cms.ai.test(integration.id, values, name);
351
+ return result.ok && !result.account && defaultModel ? { ...result, account: defaultModel } : result;
352
+ }
353
+ }));
354
+ return {
355
+ ...config,
356
+ integrations: [...config.integrations ?? [], ...integrations],
357
+ ai: {
358
+ providers: chosen.map((p) => ({ integration: p.integration.id, generate: p.generate })),
359
+ ...options.limits ? { limits: options.limits } : {}
360
+ }
361
+ };
362
+ }, INFO);
363
+ }
364
+ export {
365
+ PROVIDERS,
366
+ aiPlugin,
367
+ anthropic,
368
+ gemini,
369
+ openai,
370
+ openaiCompatible
371
+ };
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@easy-cms/plugin-ai",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.33.0",
4
+ "description": "AI for Easy CMS: connect Claude, GPT, Gemini or an OpenAI-compatible provider on the Integrations page, for the admin's AI features",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "peerDependencies": {
19
+ "@easy-cms/core": "^1.33.0"
20
+ },
21
+ "devDependencies": {
22
+ "simple-icons": "16.36.0",
23
+ "@easy-cms/core": "^1.33.0",
24
+ "@easy-cms/db-sqlite": "^1.33.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=22.12"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/maritonx/easy-cms.git",
35
+ "directory": "packages/plugin-ai"
36
+ },
37
+ "keywords": [
38
+ "easy-cms",
39
+ "easy-cms-plugin",
40
+ "ai",
41
+ "claude",
42
+ "openai",
43
+ "gemini"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run"
49
+ }
6
50
  }