@webhouse/cms-ai 0.1.1
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 +21 -0
- package/README.md +33 -0
- package/dist/index.cjs +770 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +272 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +731 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/providers/anthropic.ts
|
|
12
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
13
|
+
var PRICING, AnthropicProvider;
|
|
14
|
+
var init_anthropic = __esm({
|
|
15
|
+
"src/providers/anthropic.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
PRICING = {
|
|
18
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
19
|
+
"claude-haiku-4-5": { input: 0.8, output: 4 },
|
|
20
|
+
"claude-opus-4-6": { input: 15, output: 75 }
|
|
21
|
+
};
|
|
22
|
+
AnthropicProvider = class {
|
|
23
|
+
name = "anthropic";
|
|
24
|
+
defaultModel = "claude-sonnet-4-6";
|
|
25
|
+
client;
|
|
26
|
+
constructor(apiKey) {
|
|
27
|
+
this.client = new Anthropic({ apiKey: apiKey ?? process.env["ANTHROPIC_API_KEY"] });
|
|
28
|
+
}
|
|
29
|
+
estimateCost(inputTokens, outputTokens, model) {
|
|
30
|
+
const m = model ?? this.defaultModel;
|
|
31
|
+
const pricing = PRICING[m] ?? PRICING["claude-sonnet-4-6"];
|
|
32
|
+
return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output;
|
|
33
|
+
}
|
|
34
|
+
async generate(prompt, options = {}) {
|
|
35
|
+
const model = options.model ?? this.defaultModel;
|
|
36
|
+
const response = await this.client.messages.create({
|
|
37
|
+
model,
|
|
38
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
39
|
+
temperature: options.temperature ?? 0.7,
|
|
40
|
+
system: options.systemPrompt ?? "You are a helpful content writer.",
|
|
41
|
+
messages: [{ role: "user", content: prompt }]
|
|
42
|
+
});
|
|
43
|
+
const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
44
|
+
const inputTokens = response.usage.input_tokens;
|
|
45
|
+
const outputTokens = response.usage.output_tokens;
|
|
46
|
+
return {
|
|
47
|
+
text,
|
|
48
|
+
inputTokens,
|
|
49
|
+
outputTokens,
|
|
50
|
+
model,
|
|
51
|
+
provider: "anthropic",
|
|
52
|
+
estimatedCostUsd: this.estimateCost(inputTokens, outputTokens, model)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// src/providers/openai.ts
|
|
60
|
+
import OpenAI from "openai";
|
|
61
|
+
var PRICING2, OpenAiProvider;
|
|
62
|
+
var init_openai = __esm({
|
|
63
|
+
"src/providers/openai.ts"() {
|
|
64
|
+
"use strict";
|
|
65
|
+
PRICING2 = {
|
|
66
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
67
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 }
|
|
68
|
+
};
|
|
69
|
+
OpenAiProvider = class {
|
|
70
|
+
name = "openai";
|
|
71
|
+
defaultModel = "gpt-4o";
|
|
72
|
+
client;
|
|
73
|
+
constructor(apiKey) {
|
|
74
|
+
this.client = new OpenAI({ apiKey: apiKey ?? process.env["OPENAI_API_KEY"] });
|
|
75
|
+
}
|
|
76
|
+
estimateCost(inputTokens, outputTokens, model) {
|
|
77
|
+
const m = model ?? this.defaultModel;
|
|
78
|
+
const pricing = PRICING2[m] ?? PRICING2["gpt-4o"];
|
|
79
|
+
return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output;
|
|
80
|
+
}
|
|
81
|
+
async generate(prompt, options = {}) {
|
|
82
|
+
const model = options.model ?? this.defaultModel;
|
|
83
|
+
const response = await this.client.chat.completions.create({
|
|
84
|
+
model,
|
|
85
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
86
|
+
temperature: options.temperature ?? 0.7,
|
|
87
|
+
messages: [
|
|
88
|
+
...options.systemPrompt ? [{ role: "system", content: options.systemPrompt }] : [],
|
|
89
|
+
{ role: "user", content: prompt }
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
const text = response.choices[0]?.message.content ?? "";
|
|
93
|
+
const inputTokens = response.usage?.prompt_tokens ?? 0;
|
|
94
|
+
const outputTokens = response.usage?.completion_tokens ?? 0;
|
|
95
|
+
return {
|
|
96
|
+
text,
|
|
97
|
+
inputTokens,
|
|
98
|
+
outputTokens,
|
|
99
|
+
model,
|
|
100
|
+
provider: "openai",
|
|
101
|
+
estimatedCostUsd: this.estimateCost(inputTokens, outputTokens, model)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// src/providers/registry.ts
|
|
109
|
+
var registry_exports = {};
|
|
110
|
+
__export(registry_exports, {
|
|
111
|
+
ProviderRegistry: () => ProviderRegistry
|
|
112
|
+
});
|
|
113
|
+
var ProviderRegistry;
|
|
114
|
+
var init_registry = __esm({
|
|
115
|
+
"src/providers/registry.ts"() {
|
|
116
|
+
"use strict";
|
|
117
|
+
init_anthropic();
|
|
118
|
+
init_openai();
|
|
119
|
+
ProviderRegistry = class {
|
|
120
|
+
providers = /* @__PURE__ */ new Map();
|
|
121
|
+
defaultProviderName;
|
|
122
|
+
constructor(config = {}) {
|
|
123
|
+
if (config.anthropic !== void 0 || process.env["ANTHROPIC_API_KEY"]) {
|
|
124
|
+
this.providers.set("anthropic", new AnthropicProvider(config.anthropic?.apiKey));
|
|
125
|
+
}
|
|
126
|
+
if (config.openai !== void 0 || process.env["OPENAI_API_KEY"]) {
|
|
127
|
+
this.providers.set("openai", new OpenAiProvider(config.openai?.apiKey));
|
|
128
|
+
}
|
|
129
|
+
this.defaultProviderName = config.defaultProvider ?? (this.providers.has("anthropic") ? "anthropic" : "openai");
|
|
130
|
+
}
|
|
131
|
+
get(name) {
|
|
132
|
+
const providerName = name ?? this.defaultProviderName;
|
|
133
|
+
const provider = this.providers.get(providerName);
|
|
134
|
+
if (!provider) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`AI provider "${providerName}" not configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return provider;
|
|
140
|
+
}
|
|
141
|
+
list() {
|
|
142
|
+
return [...this.providers.keys()];
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// src/agents/content.ts
|
|
149
|
+
var content_exports = {};
|
|
150
|
+
__export(content_exports, {
|
|
151
|
+
ContentAgent: () => ContentAgent
|
|
152
|
+
});
|
|
153
|
+
var ContentAgent;
|
|
154
|
+
var init_content = __esm({
|
|
155
|
+
"src/agents/content.ts"() {
|
|
156
|
+
"use strict";
|
|
157
|
+
ContentAgent = class {
|
|
158
|
+
constructor(provider) {
|
|
159
|
+
this.provider = provider;
|
|
160
|
+
}
|
|
161
|
+
async generate(prompt, options) {
|
|
162
|
+
const { collection, locale = "en", tone, targetAudience } = options;
|
|
163
|
+
const fieldDescriptions = collection.fields.map((f) => {
|
|
164
|
+
const hint = f.ai?.hint ? ` (${f.ai.hint})` : "";
|
|
165
|
+
const maxLen = f.ai?.maxLength ?? f.maxLength;
|
|
166
|
+
const lenHint = maxLen ? ` (max ${maxLen} characters)` : "";
|
|
167
|
+
return `- "${f.name}" (${f.type})${hint}${lenHint}${f.required === true ? " [required]" : ""}`;
|
|
168
|
+
}).join("\n");
|
|
169
|
+
const systemPrompt = [
|
|
170
|
+
`You are a professional content writer creating content for a CMS.`,
|
|
171
|
+
`Collection: ${collection.label ?? collection.name}`,
|
|
172
|
+
tone ? `Tone: ${tone}` : null,
|
|
173
|
+
targetAudience ? `Target audience: ${targetAudience}` : null,
|
|
174
|
+
locale !== "en" ? `Language: ${locale}` : null
|
|
175
|
+
].filter(Boolean).join("\n");
|
|
176
|
+
const userPrompt = `Create content for the following request: "${prompt}"
|
|
177
|
+
|
|
178
|
+
Return a JSON object with these fields:
|
|
179
|
+
${fieldDescriptions}
|
|
180
|
+
|
|
181
|
+
For "richtext" fields, use Markdown formatting.
|
|
182
|
+
For "date" fields, use ISO 8601 format (e.g. "${(/* @__PURE__ */ new Date()).toISOString()}").
|
|
183
|
+
For "slug", generate a URL-friendly slug from the title (lowercase, hyphens).
|
|
184
|
+
|
|
185
|
+
Return ONLY valid JSON, no explanation, no markdown code blocks.`;
|
|
186
|
+
const result = await this.provider.generate(userPrompt, { ...options, systemPrompt });
|
|
187
|
+
let fields;
|
|
188
|
+
try {
|
|
189
|
+
const cleaned = result.text.replace(/^```(?:json)?\n?/m, "").replace(/\n?```$/m, "").trim();
|
|
190
|
+
fields = JSON.parse(cleaned);
|
|
191
|
+
} catch {
|
|
192
|
+
throw new Error(`AI returned invalid JSON: ${result.text.slice(0, 200)}`);
|
|
193
|
+
}
|
|
194
|
+
const slug = String(fields["slug"] ?? fields["title"] ?? "untitled").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
195
|
+
return {
|
|
196
|
+
fields,
|
|
197
|
+
slug,
|
|
198
|
+
usage: {
|
|
199
|
+
inputTokens: result.inputTokens,
|
|
200
|
+
outputTokens: result.outputTokens,
|
|
201
|
+
estimatedCostUsd: result.estimatedCostUsd
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async rewrite(currentData, options) {
|
|
206
|
+
const { instruction } = options;
|
|
207
|
+
const systemPrompt = `You are a professional content editor. Rewrite the provided content according to the instruction.`;
|
|
208
|
+
const userPrompt = `Current content:
|
|
209
|
+
${JSON.stringify(currentData, null, 2)}
|
|
210
|
+
|
|
211
|
+
Instruction: "${instruction}"
|
|
212
|
+
|
|
213
|
+
Return a JSON object with the same field names but updated content. Only update fields that are relevant to the instruction. Return ONLY valid JSON.`;
|
|
214
|
+
const result = await this.provider.generate(userPrompt, { ...options, systemPrompt });
|
|
215
|
+
let fields;
|
|
216
|
+
try {
|
|
217
|
+
const cleaned = result.text.replace(/^```(?:json)?\n?/m, "").replace(/\n?```$/m, "").trim();
|
|
218
|
+
fields = JSON.parse(cleaned);
|
|
219
|
+
} catch {
|
|
220
|
+
throw new Error(`AI returned invalid JSON: ${result.text.slice(0, 200)}`);
|
|
221
|
+
}
|
|
222
|
+
const slug = String(fields["slug"] ?? currentData["slug"] ?? "untitled").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
223
|
+
return {
|
|
224
|
+
fields,
|
|
225
|
+
slug,
|
|
226
|
+
usage: {
|
|
227
|
+
inputTokens: result.inputTokens,
|
|
228
|
+
outputTokens: result.outputTokens,
|
|
229
|
+
estimatedCostUsd: result.estimatedCostUsd
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async translate(currentData, targetLocale, options) {
|
|
234
|
+
return this.rewrite(currentData, {
|
|
235
|
+
instruction: `Translate all text content to ${targetLocale}. Keep field names in English. Keep dates, numbers, and URLs unchanged.`,
|
|
236
|
+
...options
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// src/agents/seo.ts
|
|
244
|
+
var seo_exports = {};
|
|
245
|
+
__export(seo_exports, {
|
|
246
|
+
SeoAgent: () => SeoAgent
|
|
247
|
+
});
|
|
248
|
+
var SeoAgent;
|
|
249
|
+
var init_seo = __esm({
|
|
250
|
+
"src/agents/seo.ts"() {
|
|
251
|
+
"use strict";
|
|
252
|
+
SeoAgent = class {
|
|
253
|
+
constructor(provider) {
|
|
254
|
+
this.provider = provider;
|
|
255
|
+
}
|
|
256
|
+
async optimize(doc, siteTitle, baseUrl) {
|
|
257
|
+
const title = String(doc.data["title"] ?? doc.slug);
|
|
258
|
+
const content = String(doc.data["content"] ?? doc.data["body"] ?? "").slice(0, 2e3);
|
|
259
|
+
const excerpt = String(doc.data["excerpt"] ?? "");
|
|
260
|
+
const systemPrompt = `You are an SEO expert. Generate optimized meta tags and structured data.`;
|
|
261
|
+
const userPrompt = `Document:
|
|
262
|
+
Title: ${title}
|
|
263
|
+
Excerpt: ${excerpt}
|
|
264
|
+
Content preview: ${content}
|
|
265
|
+
Site: ${siteTitle}
|
|
266
|
+
URL: ${baseUrl}/${doc.collection}/${doc.slug}/
|
|
267
|
+
|
|
268
|
+
Generate:
|
|
269
|
+
1. metaTitle: SEO-optimized title (50-60 chars)
|
|
270
|
+
2. metaDescription: Compelling description (150-160 chars)
|
|
271
|
+
3. jsonLd: JSON-LD structured data object (Article schema)
|
|
272
|
+
|
|
273
|
+
Return ONLY valid JSON like:
|
|
274
|
+
{
|
|
275
|
+
"metaTitle": "...",
|
|
276
|
+
"metaDescription": "...",
|
|
277
|
+
"jsonLd": { "@context": "https://schema.org", "@type": "Article", ... }
|
|
278
|
+
}`;
|
|
279
|
+
const result = await this.provider.generate(userPrompt, {
|
|
280
|
+
systemPrompt,
|
|
281
|
+
maxTokens: 1024,
|
|
282
|
+
temperature: 0.3
|
|
283
|
+
});
|
|
284
|
+
let parsed;
|
|
285
|
+
try {
|
|
286
|
+
const cleaned = result.text.replace(/^```(?:json)?\n?/m, "").replace(/\n?```$/m, "").trim();
|
|
287
|
+
parsed = JSON.parse(cleaned);
|
|
288
|
+
} catch {
|
|
289
|
+
parsed = {
|
|
290
|
+
metaTitle: title.slice(0, 60),
|
|
291
|
+
metaDescription: (excerpt || content).slice(0, 160),
|
|
292
|
+
jsonLd: {
|
|
293
|
+
"@context": "https://schema.org",
|
|
294
|
+
"@type": "Article",
|
|
295
|
+
headline: title,
|
|
296
|
+
description: excerpt || content.slice(0, 160)
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
...parsed,
|
|
302
|
+
usage: {
|
|
303
|
+
inputTokens: result.inputTokens,
|
|
304
|
+
outputTokens: result.outputTokens,
|
|
305
|
+
estimatedCostUsd: result.estimatedCostUsd
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// src/index.ts
|
|
314
|
+
init_anthropic();
|
|
315
|
+
init_openai();
|
|
316
|
+
init_registry();
|
|
317
|
+
init_content();
|
|
318
|
+
init_seo();
|
|
319
|
+
|
|
320
|
+
// src/orchestrator/engine.ts
|
|
321
|
+
var FAST_MODEL = "claude-haiku-4-5-20251001";
|
|
322
|
+
function buildFieldDescriptions(collection) {
|
|
323
|
+
return collection.fields.map((f) => {
|
|
324
|
+
const hint = f.ai?.hint ? ` (${f.ai.hint})` : "";
|
|
325
|
+
const maxLen = f.ai?.maxLength ?? f.maxLength;
|
|
326
|
+
const lenHint = maxLen ? ` (max ${maxLen} characters)` : "";
|
|
327
|
+
return `- "${f.name}" (${f.type})${hint}${lenHint}${f.required === true ? " [required]" : ""}`;
|
|
328
|
+
}).join("\n");
|
|
329
|
+
}
|
|
330
|
+
function resolveModel(agent, cockpit) {
|
|
331
|
+
if (cockpit.speedQuality === "fast") {
|
|
332
|
+
if (cockpit.primaryModel.startsWith("gpt") || cockpit.primaryModel.startsWith("o1")) {
|
|
333
|
+
return cockpit.primaryModel;
|
|
334
|
+
}
|
|
335
|
+
return FAST_MODEL;
|
|
336
|
+
}
|
|
337
|
+
return cockpit.primaryModel;
|
|
338
|
+
}
|
|
339
|
+
function buildSystemPrompt(agent, collection, cockpit, feedbackExamples) {
|
|
340
|
+
const parts = [];
|
|
341
|
+
parts.push(agent.systemPrompt);
|
|
342
|
+
if (agent.behavior.formality > 70) {
|
|
343
|
+
parts.push("Use formal, professional language.");
|
|
344
|
+
} else if (agent.behavior.formality < 30) {
|
|
345
|
+
parts.push("Use casual, conversational language.");
|
|
346
|
+
}
|
|
347
|
+
if (agent.behavior.verbosity > 70) {
|
|
348
|
+
parts.push("Be thorough and detailed in your writing.");
|
|
349
|
+
} else if (agent.behavior.verbosity < 30) {
|
|
350
|
+
parts.push("Be concise and to the point.");
|
|
351
|
+
}
|
|
352
|
+
if (cockpit.promptDepth === "minimal") {
|
|
353
|
+
}
|
|
354
|
+
if (cockpit.promptDepth === "medium" || cockpit.promptDepth === "deep") {
|
|
355
|
+
parts.push(`Collection: ${collection.label ?? collection.name}`);
|
|
356
|
+
parts.push(`Fields:
|
|
357
|
+
${buildFieldDescriptions(collection)}`);
|
|
358
|
+
}
|
|
359
|
+
if (cockpit.promptDepth === "deep") {
|
|
360
|
+
if (cockpit.seoWeight > 50) {
|
|
361
|
+
parts.push(
|
|
362
|
+
`SEO priority: ${cockpit.seoWeight}%. Optimize for search engines: use target keywords naturally in title, headings, and first paragraph. Write a compelling meta description under 160 characters.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (feedbackExamples && feedbackExamples.length > 0) {
|
|
366
|
+
const examples = feedbackExamples.slice(-3);
|
|
367
|
+
parts.push("Learn from these past corrections:");
|
|
368
|
+
for (const ex of examples) {
|
|
369
|
+
parts.push(`Original: ${ex.original}
|
|
370
|
+
Corrected: ${ex.corrected}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else if (cockpit.seoWeight > 50) {
|
|
374
|
+
parts.push(`Optimize content for SEO (weight: ${cockpit.seoWeight}%).`);
|
|
375
|
+
}
|
|
376
|
+
if (cockpit.speedQuality === "thorough") {
|
|
377
|
+
parts.push(
|
|
378
|
+
"After generating the content, review it yourself for accuracy, tone, grammar, and completeness. Fix any issues before returning the final version."
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return parts.join("\n\n");
|
|
382
|
+
}
|
|
383
|
+
var OrchestratorEngine = class {
|
|
384
|
+
constructor(provider) {
|
|
385
|
+
this.provider = provider;
|
|
386
|
+
}
|
|
387
|
+
async run(prompt, agent, collection, cockpit, feedbackExamples) {
|
|
388
|
+
const systemPrompt = buildSystemPrompt(agent, collection, cockpit, feedbackExamples);
|
|
389
|
+
const model = resolveModel(agent, cockpit);
|
|
390
|
+
const fieldDesc = buildFieldDescriptions(collection);
|
|
391
|
+
const userPrompt = `Create content for: "${prompt}"
|
|
392
|
+
|
|
393
|
+
Return a JSON object with these fields:
|
|
394
|
+
${fieldDesc}
|
|
395
|
+
|
|
396
|
+
For "richtext" fields, use Markdown formatting.
|
|
397
|
+
For "date" fields, use ISO 8601 format (e.g. "${(/* @__PURE__ */ new Date()).toISOString()}").
|
|
398
|
+
For "slug", generate a URL-friendly slug from the title (lowercase, hyphens).
|
|
399
|
+
|
|
400
|
+
Return ONLY valid JSON, no explanation, no markdown code blocks.`;
|
|
401
|
+
const temperature = agent.behavior.temperature / 100;
|
|
402
|
+
const result = await this.provider.generate(userPrompt, {
|
|
403
|
+
systemPrompt,
|
|
404
|
+
model,
|
|
405
|
+
temperature
|
|
406
|
+
});
|
|
407
|
+
let contentData;
|
|
408
|
+
try {
|
|
409
|
+
const cleaned = result.text.replace(/^```(?:json)?\n?/m, "").replace(/\n?```$/m, "").trim();
|
|
410
|
+
contentData = JSON.parse(cleaned);
|
|
411
|
+
} catch {
|
|
412
|
+
throw new Error(`AI returned invalid JSON: ${result.text.slice(0, 200)}`);
|
|
413
|
+
}
|
|
414
|
+
const slug = String(contentData["slug"] ?? contentData["title"] ?? "untitled").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
415
|
+
return {
|
|
416
|
+
contentData,
|
|
417
|
+
slug,
|
|
418
|
+
costUsd: result.estimatedCostUsd
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/orchestrator/queue.ts
|
|
424
|
+
import fs from "fs/promises";
|
|
425
|
+
import path from "path";
|
|
426
|
+
import { randomUUID } from "crypto";
|
|
427
|
+
var CurationQueue = class {
|
|
428
|
+
constructor(dataDir) {
|
|
429
|
+
this.dataDir = dataDir;
|
|
430
|
+
this.filePath = path.join(dataDir, "curation-queue.json");
|
|
431
|
+
}
|
|
432
|
+
filePath;
|
|
433
|
+
async list(status) {
|
|
434
|
+
const items = await this.read();
|
|
435
|
+
if (status === void 0) return items;
|
|
436
|
+
return items.filter((i) => i.status === status);
|
|
437
|
+
}
|
|
438
|
+
async get(id) {
|
|
439
|
+
const items = await this.read();
|
|
440
|
+
return items.find((i) => i.id === id) ?? null;
|
|
441
|
+
}
|
|
442
|
+
async add(item) {
|
|
443
|
+
const items = await this.read();
|
|
444
|
+
const newItem = {
|
|
445
|
+
...item,
|
|
446
|
+
id: randomUUID(),
|
|
447
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
448
|
+
};
|
|
449
|
+
items.push(newItem);
|
|
450
|
+
await this.write(items);
|
|
451
|
+
return newItem;
|
|
452
|
+
}
|
|
453
|
+
async updateStatus(id, status, feedback) {
|
|
454
|
+
const items = await this.read();
|
|
455
|
+
const idx = items.findIndex((i) => i.id === id);
|
|
456
|
+
if (idx === -1) throw new Error(`Queue item not found: ${id}`);
|
|
457
|
+
const item = items[idx];
|
|
458
|
+
item.status = status;
|
|
459
|
+
if (feedback !== void 0) {
|
|
460
|
+
item.rejectionFeedback = feedback;
|
|
461
|
+
}
|
|
462
|
+
await this.write(items);
|
|
463
|
+
return item;
|
|
464
|
+
}
|
|
465
|
+
async approve(id, cmsInstance) {
|
|
466
|
+
const item = await this.updateStatus(id, "approved");
|
|
467
|
+
await cmsInstance.content.create(item.collection, { ...item.contentData, status: "published" });
|
|
468
|
+
return await this.updateStatus(id, "published");
|
|
469
|
+
}
|
|
470
|
+
async reject(id, feedback) {
|
|
471
|
+
return this.updateStatus(id, "rejected", feedback);
|
|
472
|
+
}
|
|
473
|
+
async getStats() {
|
|
474
|
+
const items = await this.read();
|
|
475
|
+
return {
|
|
476
|
+
ready: items.filter((i) => i.status === "ready").length,
|
|
477
|
+
in_review: items.filter((i) => i.status === "in_review").length,
|
|
478
|
+
approved: items.filter((i) => i.status === "approved").length,
|
|
479
|
+
rejected: items.filter((i) => i.status === "rejected").length,
|
|
480
|
+
published: items.filter((i) => i.status === "published").length
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
async read() {
|
|
484
|
+
try {
|
|
485
|
+
const raw = await fs.readFile(this.filePath, "utf-8");
|
|
486
|
+
return JSON.parse(raw);
|
|
487
|
+
} catch {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async write(items) {
|
|
492
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
493
|
+
await fs.writeFile(this.filePath, JSON.stringify(items, null, 2));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// src/orchestrator/runner.ts
|
|
498
|
+
import fs2 from "fs/promises";
|
|
499
|
+
import path2 from "path";
|
|
500
|
+
var AgentRunner = class {
|
|
501
|
+
constructor(engine, queue, dataDir) {
|
|
502
|
+
this.engine = engine;
|
|
503
|
+
this.queue = queue;
|
|
504
|
+
this.dataDir = dataDir;
|
|
505
|
+
}
|
|
506
|
+
async run(agent, collection, cockpit, prompt) {
|
|
507
|
+
const feedbackExamples = await this.loadFeedback(agent.id);
|
|
508
|
+
const result = await this.engine.run(prompt, agent, collection, cockpit, feedbackExamples);
|
|
509
|
+
const shouldAutoPublish = agent.autonomy === "full" && agent.stats.totalGenerated >= 20 && agent.stats.approved + agent.stats.rejected > 0 && agent.stats.approved / (agent.stats.approved + agent.stats.rejected) > 0.95;
|
|
510
|
+
const status = shouldAutoPublish ? "approved" : "ready";
|
|
511
|
+
const title = typeof result.contentData["title"] === "string" ? result.contentData["title"] : result.slug;
|
|
512
|
+
const queueItem = await this.queue.add({
|
|
513
|
+
agentId: agent.id,
|
|
514
|
+
agentName: agent.name,
|
|
515
|
+
collection: collection.name,
|
|
516
|
+
slug: result.slug,
|
|
517
|
+
title,
|
|
518
|
+
status,
|
|
519
|
+
contentData: result.contentData,
|
|
520
|
+
costUsd: result.costUsd
|
|
521
|
+
});
|
|
522
|
+
return {
|
|
523
|
+
queueItemId: queueItem.id,
|
|
524
|
+
agentId: agent.id,
|
|
525
|
+
collection: collection.name,
|
|
526
|
+
slug: result.slug,
|
|
527
|
+
title,
|
|
528
|
+
contentData: result.contentData,
|
|
529
|
+
costUsd: result.costUsd,
|
|
530
|
+
publishedDirectly: shouldAutoPublish
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
async loadFeedback(agentId) {
|
|
534
|
+
const feedbackPath = path2.join(this.dataDir, "agents", agentId, "feedback.json");
|
|
535
|
+
try {
|
|
536
|
+
const raw = await fs2.readFile(feedbackPath, "utf-8");
|
|
537
|
+
return JSON.parse(raw);
|
|
538
|
+
} catch {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/orchestrator/scheduler.ts
|
|
545
|
+
var WINDOW_MINUTES = 5;
|
|
546
|
+
var AgentScheduler = class {
|
|
547
|
+
shouldRun(agent, now2) {
|
|
548
|
+
if (!agent.active || !agent.schedule.enabled) return false;
|
|
549
|
+
if (agent.schedule.frequency === "manual") return false;
|
|
550
|
+
if (agent.schedule.frequency === "weekly" && now2.getDay() !== 1) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
const { hours, minutes } = this.parseTime(agent.schedule.time);
|
|
554
|
+
const nowMinutes = now2.getHours() * 60 + now2.getMinutes();
|
|
555
|
+
const scheduleMinutes = hours * 60 + minutes;
|
|
556
|
+
const diff = Math.abs(nowMinutes - scheduleMinutes);
|
|
557
|
+
return diff <= WINDOW_MINUTES || diff >= 1440 - WINDOW_MINUTES;
|
|
558
|
+
}
|
|
559
|
+
getDueAgents(agents, now2) {
|
|
560
|
+
const timestamp = now2 ?? /* @__PURE__ */ new Date();
|
|
561
|
+
return agents.filter((a) => this.shouldRun(a, timestamp));
|
|
562
|
+
}
|
|
563
|
+
parseTime(timeStr) {
|
|
564
|
+
const [h, m] = timeStr.split(":");
|
|
565
|
+
return {
|
|
566
|
+
hours: parseInt(h ?? "0", 10),
|
|
567
|
+
minutes: parseInt(m ?? "0", 10)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// src/budget/tracker.ts
|
|
573
|
+
import fs3 from "fs/promises";
|
|
574
|
+
import path3 from "path";
|
|
575
|
+
var TokenBudgetTracker = class {
|
|
576
|
+
constructor(dataDir) {
|
|
577
|
+
this.dataDir = dataDir;
|
|
578
|
+
this.filePath = path3.join(dataDir, "ai-budget.json");
|
|
579
|
+
}
|
|
580
|
+
filePath;
|
|
581
|
+
async record(entry) {
|
|
582
|
+
const data = await this.read();
|
|
583
|
+
data.entries.push({
|
|
584
|
+
...entry,
|
|
585
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
586
|
+
});
|
|
587
|
+
await this.write(data);
|
|
588
|
+
}
|
|
589
|
+
async getCurrentMonthSpent() {
|
|
590
|
+
const data = await this.read();
|
|
591
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
592
|
+
const year = now2.getFullYear();
|
|
593
|
+
const month = now2.getMonth();
|
|
594
|
+
return data.entries.filter((e) => {
|
|
595
|
+
const d = new Date(e.date);
|
|
596
|
+
return d.getFullYear() === year && d.getMonth() === month;
|
|
597
|
+
}).reduce((sum, e) => sum + e.costUsd, 0);
|
|
598
|
+
}
|
|
599
|
+
async isOverBudget(monthlyLimitUsd) {
|
|
600
|
+
const spent = await this.getCurrentMonthSpent();
|
|
601
|
+
return spent >= monthlyLimitUsd;
|
|
602
|
+
}
|
|
603
|
+
async getSummary() {
|
|
604
|
+
const data = await this.read();
|
|
605
|
+
const currentMonthUsd = await this.getCurrentMonthSpent();
|
|
606
|
+
const totalUsd = data.entries.reduce((sum, e) => sum + e.costUsd, 0);
|
|
607
|
+
const byAgent = {};
|
|
608
|
+
for (const entry of data.entries) {
|
|
609
|
+
byAgent[entry.agentId] = (byAgent[entry.agentId] ?? 0) + entry.costUsd;
|
|
610
|
+
}
|
|
611
|
+
return { currentMonthUsd, totalUsd, byAgent };
|
|
612
|
+
}
|
|
613
|
+
async read() {
|
|
614
|
+
try {
|
|
615
|
+
const raw = await fs3.readFile(this.filePath, "utf-8");
|
|
616
|
+
return JSON.parse(raw);
|
|
617
|
+
} catch {
|
|
618
|
+
return { monthlyBudgetUsd: 0, entries: [] };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async write(data) {
|
|
622
|
+
await fs3.mkdir(path3.dirname(this.filePath), { recursive: true });
|
|
623
|
+
await fs3.writeFile(this.filePath, JSON.stringify(data, null, 2));
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// src/agents/defaults.ts
|
|
628
|
+
var now = (/* @__PURE__ */ new Date()).toISOString();
|
|
629
|
+
var baseStats = {
|
|
630
|
+
totalGenerated: 0,
|
|
631
|
+
approved: 0,
|
|
632
|
+
rejected: 0,
|
|
633
|
+
edited: 0
|
|
634
|
+
};
|
|
635
|
+
var baseSchedule = {
|
|
636
|
+
enabled: false,
|
|
637
|
+
frequency: "manual",
|
|
638
|
+
time: "09:00",
|
|
639
|
+
maxPerRun: 5
|
|
640
|
+
};
|
|
641
|
+
var DEFAULT_AGENTS = [
|
|
642
|
+
{
|
|
643
|
+
id: "content-writer",
|
|
644
|
+
name: "Content Writer",
|
|
645
|
+
role: "copywriter",
|
|
646
|
+
systemPrompt: "You are a professional content writer. Write engaging, well-structured content that speaks to the target audience. Use clear headings, short paragraphs, and a natural tone.",
|
|
647
|
+
behavior: { temperature: 65, formality: 50, verbosity: 60 },
|
|
648
|
+
tools: { webSearch: false, internalDatabase: true },
|
|
649
|
+
autonomy: "draft",
|
|
650
|
+
targetCollections: [],
|
|
651
|
+
schedule: { ...baseSchedule },
|
|
652
|
+
stats: { ...baseStats },
|
|
653
|
+
createdAt: now,
|
|
654
|
+
updatedAt: now,
|
|
655
|
+
active: true
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
id: "seo-optimizer",
|
|
659
|
+
name: "SEO Optimizer",
|
|
660
|
+
role: "seo",
|
|
661
|
+
systemPrompt: "You are an SEO specialist. Optimize existing content for search engines without compromising readability. Focus on keywords, meta descriptions, heading structure, and internal linking.",
|
|
662
|
+
behavior: { temperature: 30, formality: 60, verbosity: 40 },
|
|
663
|
+
tools: { webSearch: true, internalDatabase: true },
|
|
664
|
+
autonomy: "draft",
|
|
665
|
+
targetCollections: [],
|
|
666
|
+
schedule: { ...baseSchedule, frequency: "weekly" },
|
|
667
|
+
stats: { ...baseStats },
|
|
668
|
+
createdAt: now,
|
|
669
|
+
updatedAt: now,
|
|
670
|
+
active: true
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
id: "translator",
|
|
674
|
+
name: "Translator",
|
|
675
|
+
role: "translator",
|
|
676
|
+
systemPrompt: "You are a professional translator. Translate content naturally and idiomatically into the target language. Preserve meaning, tone, and formatting. Adapt cultural references where relevant.",
|
|
677
|
+
behavior: { temperature: 20, formality: 50, verbosity: 50 },
|
|
678
|
+
tools: { webSearch: false, internalDatabase: true },
|
|
679
|
+
autonomy: "draft",
|
|
680
|
+
targetCollections: [],
|
|
681
|
+
schedule: { ...baseSchedule },
|
|
682
|
+
stats: { ...baseStats },
|
|
683
|
+
createdAt: now,
|
|
684
|
+
updatedAt: now,
|
|
685
|
+
active: true
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
id: "content-refresher",
|
|
689
|
+
name: "Content Refresher",
|
|
690
|
+
role: "refresher",
|
|
691
|
+
systemPrompt: "You are a specialist in updating and refreshing existing content. Find outdated information, update statistics and facts, improve phrasing, and add relevant new content. Preserve the original tone and structure.",
|
|
692
|
+
behavior: { temperature: 40, formality: 50, verbosity: 50 },
|
|
693
|
+
tools: { webSearch: true, internalDatabase: true },
|
|
694
|
+
autonomy: "draft",
|
|
695
|
+
targetCollections: [],
|
|
696
|
+
schedule: { ...baseSchedule, frequency: "weekly", time: "06:00" },
|
|
697
|
+
stats: { ...baseStats },
|
|
698
|
+
createdAt: now,
|
|
699
|
+
updatedAt: now,
|
|
700
|
+
active: true
|
|
701
|
+
}
|
|
702
|
+
];
|
|
703
|
+
|
|
704
|
+
// src/index.ts
|
|
705
|
+
async function createAi(config = {}) {
|
|
706
|
+
const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
|
|
707
|
+
const { ContentAgent: ContentAgent2 } = await Promise.resolve().then(() => (init_content(), content_exports));
|
|
708
|
+
const { SeoAgent: SeoAgent2 } = await Promise.resolve().then(() => (init_seo(), seo_exports));
|
|
709
|
+
const registry = new ProviderRegistry2(config);
|
|
710
|
+
const provider = registry.get();
|
|
711
|
+
return {
|
|
712
|
+
registry,
|
|
713
|
+
content: new ContentAgent2(provider),
|
|
714
|
+
seo: new SeoAgent2(provider)
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
export {
|
|
718
|
+
AgentRunner,
|
|
719
|
+
AgentScheduler,
|
|
720
|
+
AnthropicProvider,
|
|
721
|
+
ContentAgent,
|
|
722
|
+
CurationQueue,
|
|
723
|
+
DEFAULT_AGENTS,
|
|
724
|
+
OpenAiProvider,
|
|
725
|
+
OrchestratorEngine,
|
|
726
|
+
ProviderRegistry,
|
|
727
|
+
SeoAgent,
|
|
728
|
+
TokenBudgetTracker,
|
|
729
|
+
createAi
|
|
730
|
+
};
|
|
731
|
+
//# sourceMappingURL=index.js.map
|