@kubuild/ai 0.3.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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-27AY2KBI.js +500 -0
  3. package/dist/chunk-27AY2KBI.js.map +1 -0
  4. package/dist/chunk-JY4NHD3Q.js +227 -0
  5. package/dist/chunk-JY4NHD3Q.js.map +1 -0
  6. package/dist/chunk-UH24LHRY.js +443 -0
  7. package/dist/chunk-UH24LHRY.js.map +1 -0
  8. package/dist/client/ai-client.d.ts +59 -0
  9. package/dist/client/ai-client.d.ts.map +1 -0
  10. package/dist/client/index.cjs +254 -0
  11. package/dist/client/index.cjs.map +1 -0
  12. package/dist/client/index.d.ts +2 -0
  13. package/dist/client/index.d.ts.map +1 -0
  14. package/dist/client/index.js +9 -0
  15. package/dist/client/index.js.map +1 -0
  16. package/dist/core/index.d.ts +3 -0
  17. package/dist/core/index.d.ts.map +1 -0
  18. package/dist/core/normalizer.d.ts +9 -0
  19. package/dist/core/normalizer.d.ts.map +1 -0
  20. package/dist/core/prompt-compiler.d.ts +10 -0
  21. package/dist/core/prompt-compiler.d.ts.map +1 -0
  22. package/dist/index.cjs +468 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +23 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/react/index.cjs +584 -0
  29. package/dist/react/index.cjs.map +1 -0
  30. package/dist/react/index.d.ts +3 -0
  31. package/dist/react/index.d.ts.map +1 -0
  32. package/dist/react/index.js +336 -0
  33. package/dist/react/index.js.map +1 -0
  34. package/dist/react/use-ai-chat.d.ts +63 -0
  35. package/dist/react/use-ai-chat.d.ts.map +1 -0
  36. package/dist/react/use-ai-generator.d.ts +19 -0
  37. package/dist/react/use-ai-generator.d.ts.map +1 -0
  38. package/dist/server/adapters/anthropic.d.ts +30 -0
  39. package/dist/server/adapters/anthropic.d.ts.map +1 -0
  40. package/dist/server/adapters/base.d.ts +2 -0
  41. package/dist/server/adapters/base.d.ts.map +1 -0
  42. package/dist/server/adapters/custom.d.ts +18 -0
  43. package/dist/server/adapters/custom.d.ts.map +1 -0
  44. package/dist/server/adapters/gemini.d.ts +22 -0
  45. package/dist/server/adapters/gemini.d.ts.map +1 -0
  46. package/dist/server/adapters/index.cjs +529 -0
  47. package/dist/server/adapters/index.cjs.map +1 -0
  48. package/dist/server/adapters/index.d.ts +6 -0
  49. package/dist/server/adapters/index.d.ts.map +1 -0
  50. package/dist/server/adapters/index.js +13 -0
  51. package/dist/server/adapters/index.js.map +1 -0
  52. package/dist/server/adapters/openai.d.ts +26 -0
  53. package/dist/server/adapters/openai.d.ts.map +1 -0
  54. package/dist/server/adapters/stream-utils.d.ts +12 -0
  55. package/dist/server/adapters/stream-utils.d.ts.map +1 -0
  56. package/dist/server/engine.d.ts +59 -0
  57. package/dist/server/engine.d.ts.map +1 -0
  58. package/dist/server/handler.d.ts +66 -0
  59. package/dist/server/handler.d.ts.map +1 -0
  60. package/dist/server/index.cjs +1824 -0
  61. package/dist/server/index.cjs.map +1 -0
  62. package/dist/server/index.d.ts +4 -0
  63. package/dist/server/index.d.ts.map +1 -0
  64. package/dist/server/index.js +890 -0
  65. package/dist/server/index.js.map +1 -0
  66. package/dist/types.d.ts +184 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/package.json +94 -0
@@ -0,0 +1,890 @@
1
+ import {
2
+ buildJsonSchemaForMode,
3
+ buildSystemPrompt,
4
+ compileComponentCatalog,
5
+ extractJsonFromResponse,
6
+ normalizeAndValidatePageDocument,
7
+ normalizeAndValidateRefactoredNode,
8
+ normalizeAndValidateSectionNode
9
+ } from "../chunk-UH24LHRY.js";
10
+ import {
11
+ AnthropicAdapter,
12
+ CustomHttpAdapter,
13
+ GeminiAdapter,
14
+ OpenAiAdapter
15
+ } from "../chunk-27AY2KBI.js";
16
+
17
+ // src/server/engine.ts
18
+ import { DocumentMetadataSchema } from "@kubuild/schema";
19
+ var KubuildAiEngine = class {
20
+ options;
21
+ catalog;
22
+ constructor(options) {
23
+ if (!options.adapter) {
24
+ throw new Error("KubuildAiEngine requires an adapter instance");
25
+ }
26
+ this.options = options;
27
+ this.catalog = compileComponentCatalog(options.registry);
28
+ }
29
+ get adapterName() {
30
+ return this.options.adapter.name;
31
+ }
32
+ refreshCatalog() {
33
+ this.catalog = compileComponentCatalog(this.options.registry);
34
+ }
35
+ log(level, message, meta) {
36
+ if (this.options.logger) {
37
+ this.options.logger(level, message, meta);
38
+ } else if (this.options.debug) {
39
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
40
+ console.log(`[KUBUILD-AI ${level.toUpperCase()}] [${ts}] ${message}`, meta !== void 0 ? meta : "");
41
+ }
42
+ }
43
+ async generatePage(request, context) {
44
+ let rawText = "";
45
+ try {
46
+ this.log("info", `Generating full page for prompt: "${request.prompt}"`);
47
+ const systemPrompt = buildSystemPrompt({
48
+ catalog: this.catalog,
49
+ mode: "full-page",
50
+ prefix: this.options.systemPromptPrefix,
51
+ stylePreference: request.stylePreference
52
+ });
53
+ let userPrompt = `User Request: ${request.prompt}`;
54
+ if (request.tone) userPrompt += `
55
+ Tone: ${request.tone}`;
56
+ if (request.locale) userPrompt += `
57
+ Language/Locale: ${request.locale}`;
58
+ const jsonSchema = buildJsonSchemaForMode("full-page");
59
+ const result = await this.options.adapter.generate({
60
+ systemPrompt,
61
+ userPrompt,
62
+ jsonSchema,
63
+ signal: context?.signal
64
+ });
65
+ this.log("debug", "Raw model response (generatePage)", result.text);
66
+ rawText = result.text;
67
+ const rawJson = extractJsonFromResponse(rawText);
68
+ const document = normalizeAndValidatePageDocument(
69
+ rawJson,
70
+ this.options.securityLimits
71
+ );
72
+ if (request.metadata) {
73
+ document.metadata = DocumentMetadataSchema.parse({
74
+ ...document.metadata,
75
+ ...request.metadata
76
+ });
77
+ }
78
+ this.log(
79
+ "info",
80
+ `Full page successfully generated: "${document.metadata?.title || "Untitled"}" (${document.document.children?.length || 0} sections)`
81
+ );
82
+ return {
83
+ success: true,
84
+ data: document,
85
+ usage: result.usage,
86
+ rawModelResponse: rawText
87
+ };
88
+ } catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ this.log("error", `Generation failed: ${message}`);
91
+ return {
92
+ success: false,
93
+ error: {
94
+ code: "GENERATION_ERROR",
95
+ message
96
+ },
97
+ rawModelResponse: rawText || void 0
98
+ };
99
+ }
100
+ }
101
+ async generateSection(request, context) {
102
+ let rawText = "";
103
+ try {
104
+ const systemPrompt = buildSystemPrompt({
105
+ catalog: this.catalog,
106
+ mode: "section",
107
+ prefix: this.options.systemPromptPrefix,
108
+ stylePreference: request.stylePreference
109
+ });
110
+ let userPrompt = `Generate a single section node for: ${request.prompt}`;
111
+ if (request.targetSectionType) {
112
+ userPrompt += `
113
+ Target Section Purpose: ${request.targetSectionType}`;
114
+ }
115
+ if (request.parentContext) {
116
+ userPrompt += `
117
+ Surrounding Page Context: ${request.parentContext}`;
118
+ }
119
+ const jsonSchema = buildJsonSchemaForMode("section");
120
+ const result = await this.options.adapter.generate({
121
+ systemPrompt,
122
+ userPrompt,
123
+ jsonSchema,
124
+ signal: context?.signal
125
+ });
126
+ this.log("debug", "Raw model response (generateSection)", result.text);
127
+ rawText = result.text;
128
+ const rawJson = extractJsonFromResponse(rawText);
129
+ const sectionNode = normalizeAndValidateSectionNode(
130
+ rawJson,
131
+ this.options.securityLimits
132
+ );
133
+ return {
134
+ success: true,
135
+ data: sectionNode,
136
+ usage: result.usage,
137
+ rawModelResponse: rawText
138
+ };
139
+ } catch (err) {
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ return {
142
+ success: false,
143
+ error: {
144
+ code: "SECTION_GENERATION_ERROR",
145
+ message
146
+ },
147
+ rawModelResponse: rawText || void 0
148
+ };
149
+ }
150
+ }
151
+ async refactorNode(request, context) {
152
+ let rawText = "";
153
+ try {
154
+ const systemPrompt = buildSystemPrompt({
155
+ catalog: this.catalog,
156
+ mode: "refactor",
157
+ prefix: this.options.systemPromptPrefix,
158
+ stylePreference: request.stylePreference
159
+ });
160
+ const userPrompt = `Instruction: ${request.instruction}
161
+
162
+ Current Node:
163
+ ${JSON.stringify(request.node, null, 2)}`;
164
+ const jsonSchema = buildJsonSchemaForMode("refactor");
165
+ const result = await this.options.adapter.generate({
166
+ systemPrompt,
167
+ userPrompt,
168
+ jsonSchema,
169
+ signal: context?.signal
170
+ });
171
+ rawText = result.text;
172
+ const rawJson = extractJsonFromResponse(rawText);
173
+ const updatedNode = normalizeAndValidateRefactoredNode(
174
+ rawJson,
175
+ request.node,
176
+ this.options.securityLimits
177
+ );
178
+ return {
179
+ success: true,
180
+ data: updatedNode,
181
+ usage: result.usage,
182
+ rawModelResponse: rawText
183
+ };
184
+ } catch (err) {
185
+ const message = err instanceof Error ? err.message : String(err);
186
+ return {
187
+ success: false,
188
+ error: {
189
+ code: "REFACTOR_ERROR",
190
+ message
191
+ },
192
+ rawModelResponse: rawText || void 0
193
+ };
194
+ }
195
+ }
196
+ /**
197
+ * Progressive Section Streaming Generator.
198
+ * Emits structured SSE events: status -> metadata -> section (one by one) -> complete.
199
+ */
200
+ async *streamPage(request, context) {
201
+ try {
202
+ this.log("info", `[SSE] Starting streamPage for prompt: "${request.prompt}"`);
203
+ yield {
204
+ type: "status",
205
+ message: "Analyzing requirements and planning page sections..."
206
+ };
207
+ const planSystemPrompt = `
208
+ You are a web architect for the KUBUILD page builder.
209
+ Given the user's prompt, plan the website structure. Output pure JSON (no markdown fences, no explanatory text):
210
+ {
211
+ "title": "<Page Title>",
212
+ "description": "<Page Description>",
213
+ "pageStyles": {
214
+ "base": {
215
+ "backgroundColor": "#ffffff",
216
+ "fontFamily": "Inter, system-ui, sans-serif",
217
+ "color": "#111827",
218
+ "minHeight": "100vh"
219
+ }
220
+ },
221
+ "sections": [
222
+ {
223
+ "type": "<hero|features|pricing|testimonials|cta|contact|footer>",
224
+ "title": "<Brief title>",
225
+ "prompt": "<Detailed prompt for generating this specific section with components, layout, and content>"
226
+ }
227
+ ]
228
+ }
229
+ Plan 3 cohesive, essential sections (e.g., hero, features, and cta) that fulfill the request.
230
+ `;
231
+ let planUserPrompt = `User Request: ${request.prompt}`;
232
+ if (request.stylePreference) {
233
+ planUserPrompt += `
234
+ Style Preference: ${request.stylePreference}`;
235
+ }
236
+ if (request.tone) planUserPrompt += `
237
+ Tone: ${request.tone}`;
238
+ if (request.locale) planUserPrompt += `
239
+ Locale: ${request.locale}`;
240
+ this.log("info", "[SSE] Generating website layout plan...");
241
+ const planResult = await this.options.adapter.generate({
242
+ systemPrompt: planSystemPrompt,
243
+ userPrompt: planUserPrompt,
244
+ signal: context?.signal
245
+ });
246
+ this.log("debug", "[SSE] Raw plan response from model", planResult.text);
247
+ let plan;
248
+ try {
249
+ plan = extractJsonFromResponse(planResult.text);
250
+ this.log("debug", "[SSE] Parsed plan successfully", plan);
251
+ } catch (parseErr) {
252
+ this.log("warn", "[SSE] Failed to parse plan JSON, using fallback plan", parseErr);
253
+ plan = {
254
+ title: "AI Generated Page",
255
+ description: "Generated by KUBUILD AI",
256
+ sections: [
257
+ { type: "hero", title: "Hero Banner", prompt: `Hero section for: ${request.prompt}` },
258
+ { type: "features", title: "Key Features", prompt: `Features grid for: ${request.prompt}` },
259
+ { type: "cta", title: "Call To Action", prompt: `Call to action section for: ${request.prompt}` }
260
+ ]
261
+ };
262
+ }
263
+ const sectionsToGenerate = Array.isArray(plan.sections) && plan.sections.length > 0 ? plan.sections : [
264
+ { type: "hero", title: "Hero Section", prompt: `Hero banner for: ${request.prompt}` },
265
+ { type: "features", title: "Features", prompt: `Features grid for: ${request.prompt}` },
266
+ { type: "cta", title: "Call To Action", prompt: `CTA section for: ${request.prompt}` }
267
+ ];
268
+ const metadata = DocumentMetadataSchema.parse({
269
+ title: plan.title || "AI Generated Page",
270
+ description: plan.description || "Generated by KUBUILD AI",
271
+ author: "KUBUILD AI",
272
+ tags: ["ai-generated", "progressive-stream"],
273
+ category: "landing",
274
+ version: "1.0.0",
275
+ ...request.metadata || {}
276
+ });
277
+ const rootPageNode = {
278
+ id: "root-page",
279
+ type: "page",
280
+ styles: plan.pageStyles || {
281
+ base: {
282
+ backgroundColor: "#ffffff",
283
+ fontFamily: "Inter, system-ui, sans-serif",
284
+ color: "#111827",
285
+ minHeight: "100vh"
286
+ }
287
+ },
288
+ children: []
289
+ };
290
+ this.log("info", `[SSE] Emitting metadata: "${metadata.title}"`);
291
+ yield {
292
+ type: "metadata",
293
+ metadata,
294
+ rootPageNode
295
+ };
296
+ this.log(
297
+ "info",
298
+ `[SSE] Plan ready with ${sectionsToGenerate.length} sections: ${sectionsToGenerate.map((s) => s.title).join(", ")}`
299
+ );
300
+ yield {
301
+ type: "status",
302
+ message: `Plan ready with ${sectionsToGenerate.length} sections: ${sectionsToGenerate.map((s) => s.title).join(", ")}`
303
+ };
304
+ const completedSections = [];
305
+ const total = sectionsToGenerate.length;
306
+ for (let i = 0; i < total; i++) {
307
+ if (context?.signal?.aborted) {
308
+ this.log("warn", "[SSE] Streaming aborted by client signal");
309
+ throw new Error("Streaming aborted by client");
310
+ }
311
+ const plannedSec = sectionsToGenerate[i];
312
+ this.log(
313
+ "info",
314
+ `[SSE] Generating section ${i + 1}/${total} [${plannedSec.type}]: "${plannedSec.title}"`
315
+ );
316
+ yield {
317
+ type: "status",
318
+ message: `Generating section ${i + 1}/${total} (${plannedSec.title})...`
319
+ };
320
+ const secRes = await this.generateSection(
321
+ {
322
+ prompt: plannedSec.prompt,
323
+ stylePreference: request.stylePreference,
324
+ targetSectionType: plannedSec.type,
325
+ parentContext: `Website: ${plan.title}. Previous sections: ${sectionsToGenerate.slice(0, i).map((s) => s.title).join(", ")}`
326
+ },
327
+ { signal: context?.signal }
328
+ );
329
+ if (secRes.success && secRes.data) {
330
+ this.log(
331
+ "info",
332
+ `[SSE] Section ${i + 1}/${total} generated successfully: [${secRes.data.id || secRes.data.type}]`
333
+ );
334
+ completedSections.push(secRes.data);
335
+ yield {
336
+ type: "section",
337
+ index: i,
338
+ total,
339
+ section: secRes.data
340
+ };
341
+ } else {
342
+ this.log("error", `[SSE] Section ${i + 1}/${total} failed`, secRes.error?.message);
343
+ }
344
+ }
345
+ if (completedSections.length === 0) {
346
+ this.log("error", "[SSE] All sections failed to generate.");
347
+ yield {
348
+ type: "error",
349
+ error: {
350
+ code: "STREAM_GENERATION_FAILED",
351
+ message: "All sections failed to generate. Please check server logs or retry."
352
+ }
353
+ };
354
+ return;
355
+ }
356
+ const finalDocument = {
357
+ schema: "stora.page",
358
+ version: "1.0.0",
359
+ metadata,
360
+ document: {
361
+ ...rootPageNode,
362
+ children: completedSections
363
+ }
364
+ };
365
+ this.log(
366
+ "info",
367
+ `[SSE] All ${completedSections.length} sections generated. Emitting complete event.`
368
+ );
369
+ yield {
370
+ type: "complete",
371
+ document: finalDocument
372
+ };
373
+ } catch (err) {
374
+ const message = err instanceof Error ? err.message : String(err);
375
+ this.log("error", `[SSE] Streaming failed: ${message}`);
376
+ yield {
377
+ type: "error",
378
+ error: {
379
+ code: "STREAM_ERROR",
380
+ message
381
+ }
382
+ };
383
+ }
384
+ }
385
+ /**
386
+ * Builds the system/user prompt pair shared by `chat()` and `chatStream()` — the exact
387
+ * same document/selection grounding is used regardless of whether the response is
388
+ * streamed token-by-token or returned as a single request/response (STORA-515).
389
+ */
390
+ buildChatPrompt(request) {
391
+ let systemPrompt = `You are the KUBUILD AI Assistant \u2014 an expert web designer, developer, and page architecture consultant.
392
+ Your role is to assist users with creating, designing, refining, and understanding web pages built with the KUBUILD builder.
393
+ Answer questions directly, concisely, and helpfully using friendly professional tone and markdown formatting.
394
+
395
+ ### Available KUBUILD Components:
396
+ ${this.catalog.map((c) => `- **${c.type}** (${c.category}): ${c.label}${c.description ? ` \u2014 ${c.description}` : ""}`).join("\n")}
397
+ `;
398
+ if (this.options.systemPromptPrefix) {
399
+ systemPrompt = `${this.options.systemPromptPrefix}
400
+
401
+ ${systemPrompt}`;
402
+ }
403
+ if (request.systemPrompt) {
404
+ systemPrompt += `
405
+ Additional Context:
406
+ ${request.systemPrompt}`;
407
+ }
408
+ if (request.currentDocument) {
409
+ const doc = request.currentDocument;
410
+ const sections = doc.document?.children || [];
411
+ const sectionSummary = sections.map((s, idx) => `Section ${idx + 1}: id="${s.id}", type="${s.type}", title="${s.props?.title || "untitled"}", childrenCount=${s.children?.length || 0}`).join("\n");
412
+ systemPrompt += `
413
+ ### Current Page in Builder Canvas:
414
+ - Title: ${doc.metadata?.title || "Untitled Page"}
415
+ - Description: ${doc.metadata?.description || "N/A"}
416
+ - Total Sections: ${sections.length}
417
+ - Sections Summary:
418
+ ${sectionSummary || "(Canvas is currently empty)"}
419
+ `;
420
+ }
421
+ if (request.selectedNodeId) {
422
+ systemPrompt += `
423
+ Currently Selected Component Node ID: "${request.selectedNodeId}"`;
424
+ }
425
+ const lastUserMsg = [...request.messages].reverse().find((m) => m.role === "user");
426
+ const userPrompt = lastUserMsg?.content || "Hello";
427
+ return { systemPrompt, userPrompt };
428
+ }
429
+ /**
430
+ * Conversational Assistant / Q&A.
431
+ * Can discuss web design, explain KUBUILD components, review current page,
432
+ * suggest improvements, or recommend copywriting.
433
+ *
434
+ * Single request/response — unchanged since before STORA-515. See `chatStream()` for
435
+ * the token-level streaming variant.
436
+ */
437
+ async chat(request, context) {
438
+ let rawText = "";
439
+ try {
440
+ if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
441
+ return {
442
+ success: false,
443
+ error: {
444
+ code: "INVALID_CHAT_REQUEST",
445
+ message: '"messages" array is required and must not be empty'
446
+ }
447
+ };
448
+ }
449
+ this.log("info", `Processing chat turn with ${request.messages.length} messages`);
450
+ const { systemPrompt, userPrompt } = this.buildChatPrompt(request);
451
+ const result = await this.options.adapter.generate({
452
+ systemPrompt,
453
+ userPrompt,
454
+ messages: request.messages,
455
+ signal: context?.signal
456
+ });
457
+ rawText = result.text.trim();
458
+ const assistantMessage = {
459
+ role: "assistant",
460
+ content: rawText,
461
+ timestamp: Date.now()
462
+ };
463
+ return {
464
+ success: true,
465
+ data: {
466
+ message: assistantMessage
467
+ },
468
+ usage: result.usage,
469
+ rawModelResponse: rawText
470
+ };
471
+ } catch (err) {
472
+ const message = err instanceof Error ? err.message : String(err);
473
+ this.log("error", `Chat failed: ${message}`);
474
+ return {
475
+ success: false,
476
+ error: {
477
+ code: "CHAT_ERROR",
478
+ message
479
+ },
480
+ rawModelResponse: rawText || void 0
481
+ };
482
+ }
483
+ }
484
+ /**
485
+ * Token-level streaming variant of `chat()` (STORA-515). Emits `chat-chunk` events as
486
+ * partial text arrives, then a terminal `chat-complete` event with the fully assembled
487
+ * assistant message — the same `AsyncIterable<AiStreamEvent>` mechanism `streamPage`
488
+ * already uses, so it can be piped over SSE by `createAiHandler`/consumed by
489
+ * `KubuildAiClient` with no separate transport.
490
+ *
491
+ * When the configured adapter does not implement `generateStream` (e.g. the generic
492
+ * custom/HTTP adapter), this falls back to a single non-streaming `generate()` call and
493
+ * emits the entire response as one `chat-chunk`, so callers never see an error just
494
+ * because the underlying provider can't stream — only a less granular update cadence.
495
+ */
496
+ async *chatStream(request, context) {
497
+ try {
498
+ if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
499
+ yield {
500
+ type: "error",
501
+ error: {
502
+ code: "INVALID_CHAT_REQUEST",
503
+ message: '"messages" array is required and must not be empty'
504
+ }
505
+ };
506
+ return;
507
+ }
508
+ this.log(
509
+ "info",
510
+ `[SSE] Starting chatStream with ${request.messages.length} messages (adapter: ${this.options.adapter.name})`
511
+ );
512
+ const { systemPrompt, userPrompt } = this.buildChatPrompt(request);
513
+ let accumulated = "";
514
+ let usage;
515
+ if (typeof this.options.adapter.generateStream === "function") {
516
+ const generator = this.options.adapter.generateStream({
517
+ systemPrompt,
518
+ userPrompt,
519
+ messages: request.messages,
520
+ signal: context?.signal
521
+ });
522
+ while (true) {
523
+ const { value, done } = await generator.next();
524
+ if (done) {
525
+ usage = value?.usage;
526
+ break;
527
+ }
528
+ accumulated += value;
529
+ yield { type: "chat-chunk", delta: value, content: accumulated };
530
+ }
531
+ } else {
532
+ this.log(
533
+ "debug",
534
+ `Adapter "${this.options.adapter.name}" has no generateStream; falling back to non-streaming chat`
535
+ );
536
+ const result = await this.options.adapter.generate({
537
+ systemPrompt,
538
+ userPrompt,
539
+ messages: request.messages,
540
+ signal: context?.signal
541
+ });
542
+ accumulated = result.text;
543
+ usage = result.usage;
544
+ yield { type: "chat-chunk", delta: accumulated, content: accumulated };
545
+ }
546
+ const assistantMessage = {
547
+ role: "assistant",
548
+ content: accumulated.trim(),
549
+ timestamp: Date.now()
550
+ };
551
+ this.log("info", "[SSE] Chat stream completed", usage);
552
+ yield { type: "chat-complete", message: assistantMessage };
553
+ } catch (err) {
554
+ const message = err instanceof Error ? err.message : String(err);
555
+ this.log("error", `[SSE] Chat streaming failed: ${message}`);
556
+ yield {
557
+ type: "error",
558
+ error: {
559
+ code: "CHAT_STREAM_ERROR",
560
+ message
561
+ }
562
+ };
563
+ }
564
+ }
565
+ };
566
+
567
+ // src/server/handler.ts
568
+ async function processAiRequest(engine, body, signal) {
569
+ if (!body || typeof body !== "object") {
570
+ return {
571
+ status: 400,
572
+ response: {
573
+ success: false,
574
+ error: {
575
+ code: "BAD_REQUEST",
576
+ message: "Request body must be a valid JSON object"
577
+ }
578
+ }
579
+ };
580
+ }
581
+ const payload = body;
582
+ const mode = payload.mode || "full-page";
583
+ if (mode === "full-page") {
584
+ if (!payload.prompt || typeof payload.prompt !== "string") {
585
+ return {
586
+ status: 400,
587
+ response: {
588
+ success: false,
589
+ error: {
590
+ code: "INVALID_PROMPT",
591
+ message: '"prompt" is required for full-page generation'
592
+ }
593
+ }
594
+ };
595
+ }
596
+ const result = await engine.generatePage(
597
+ {
598
+ prompt: payload.prompt,
599
+ stylePreference: payload.stylePreference,
600
+ tone: payload.tone,
601
+ locale: payload.locale,
602
+ metadata: payload.metadata
603
+ },
604
+ { signal }
605
+ );
606
+ return {
607
+ status: result.success ? 200 : 500,
608
+ response: result
609
+ };
610
+ }
611
+ if (mode === "section") {
612
+ if (!payload.prompt || typeof payload.prompt !== "string") {
613
+ return {
614
+ status: 400,
615
+ response: {
616
+ success: false,
617
+ error: {
618
+ code: "INVALID_PROMPT",
619
+ message: '"prompt" is required for section generation'
620
+ }
621
+ }
622
+ };
623
+ }
624
+ const result = await engine.generateSection(
625
+ {
626
+ prompt: payload.prompt,
627
+ stylePreference: payload.stylePreference,
628
+ targetSectionType: payload.targetSectionType,
629
+ parentContext: payload.parentContext
630
+ },
631
+ { signal }
632
+ );
633
+ return {
634
+ status: result.success ? 200 : 500,
635
+ response: result
636
+ };
637
+ }
638
+ if (mode === "refactor") {
639
+ if (!payload.node || !payload.instruction) {
640
+ return {
641
+ status: 400,
642
+ response: {
643
+ success: false,
644
+ error: {
645
+ code: "INVALID_REFACTOR_PARAMS",
646
+ message: '"node" and "instruction" are required for refactoring'
647
+ }
648
+ }
649
+ };
650
+ }
651
+ const result = await engine.refactorNode(
652
+ {
653
+ node: payload.node,
654
+ instruction: payload.instruction,
655
+ stylePreference: payload.stylePreference
656
+ },
657
+ { signal }
658
+ );
659
+ return {
660
+ status: result.success ? 200 : 500,
661
+ response: result
662
+ };
663
+ }
664
+ if (mode === "chat") {
665
+ if (!payload.messages || !Array.isArray(payload.messages) || payload.messages.length === 0) {
666
+ return {
667
+ status: 400,
668
+ response: {
669
+ success: false,
670
+ error: {
671
+ code: "INVALID_CHAT_PARAMS",
672
+ message: '"messages" array is required and must not be empty for chat mode'
673
+ }
674
+ }
675
+ };
676
+ }
677
+ const result = await engine.chat(
678
+ {
679
+ messages: payload.messages,
680
+ currentDocument: payload.currentDocument,
681
+ selectedNodeId: payload.selectedNodeId
682
+ },
683
+ { signal }
684
+ );
685
+ return {
686
+ status: result.success ? 200 : 500,
687
+ response: result
688
+ };
689
+ }
690
+ return {
691
+ status: 400,
692
+ response: {
693
+ success: false,
694
+ error: {
695
+ code: "UNKNOWN_MODE",
696
+ message: `Unsupported mode: ${String(mode)}. Supported modes: 'full-page', 'section', 'refactor', 'chat'`
697
+ }
698
+ }
699
+ };
700
+ }
701
+ function createAiHandler(engine, options) {
702
+ return async (request) => {
703
+ try {
704
+ if (options?.beforeRequest) {
705
+ const hookResult = await options.beforeRequest(request);
706
+ if (hookResult instanceof Response) {
707
+ return hookResult;
708
+ }
709
+ }
710
+ if (request.method !== "POST") {
711
+ return new Response(
712
+ JSON.stringify({
713
+ success: false,
714
+ error: {
715
+ code: "METHOD_NOT_ALLOWED",
716
+ message: "Only POST requests are accepted"
717
+ }
718
+ }),
719
+ {
720
+ status: 405,
721
+ headers: {
722
+ "Content-Type": "application/json",
723
+ Allow: "POST"
724
+ }
725
+ }
726
+ );
727
+ }
728
+ const body = await request.json().catch(() => null);
729
+ if (body && typeof body === "object" && body.stream === true) {
730
+ const mode = body.mode || "full-page";
731
+ if (mode === "chat") {
732
+ if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
733
+ return new Response(
734
+ JSON.stringify({
735
+ success: false,
736
+ error: {
737
+ code: "INVALID_CHAT_PARAMS",
738
+ message: '"messages" array is required and must not be empty for chat mode'
739
+ }
740
+ }),
741
+ {
742
+ status: 400,
743
+ headers: { "Content-Type": "application/json" }
744
+ }
745
+ );
746
+ }
747
+ const encoder2 = new TextEncoder();
748
+ const readable2 = new ReadableStream({
749
+ async start(controller) {
750
+ try {
751
+ for await (const event of engine.chatStream(
752
+ {
753
+ messages: body.messages,
754
+ currentDocument: body.currentDocument,
755
+ selectedNodeId: body.selectedNodeId
756
+ },
757
+ { signal: request.signal }
758
+ )) {
759
+ controller.enqueue(
760
+ encoder2.encode(`event: ${event.type}
761
+ data: ${JSON.stringify(event)}
762
+
763
+ `)
764
+ );
765
+ }
766
+ } catch (err) {
767
+ const message = err instanceof Error ? err.message : String(err);
768
+ controller.enqueue(
769
+ encoder2.encode(
770
+ `event: error
771
+ data: ${JSON.stringify({ type: "error", error: { code: "CHAT_STREAM_ERROR", message } })}
772
+
773
+ `
774
+ )
775
+ );
776
+ } finally {
777
+ controller.close();
778
+ }
779
+ }
780
+ });
781
+ return new Response(readable2, {
782
+ status: 200,
783
+ headers: {
784
+ "Content-Type": "text/event-stream",
785
+ "Cache-Control": "no-cache, no-transform",
786
+ Connection: "keep-alive"
787
+ }
788
+ });
789
+ }
790
+ if (!body.prompt || typeof body.prompt !== "string") {
791
+ return new Response(
792
+ JSON.stringify({
793
+ success: false,
794
+ error: {
795
+ code: "INVALID_PROMPT",
796
+ message: '"prompt" is required for streaming generation'
797
+ }
798
+ }),
799
+ {
800
+ status: 400,
801
+ headers: { "Content-Type": "application/json" }
802
+ }
803
+ );
804
+ }
805
+ const encoder = new TextEncoder();
806
+ const readable = new ReadableStream({
807
+ async start(controller) {
808
+ try {
809
+ for await (const event of engine.streamPage(
810
+ {
811
+ prompt: body.prompt,
812
+ stylePreference: body.stylePreference,
813
+ tone: body.tone,
814
+ locale: body.locale,
815
+ metadata: body.metadata
816
+ },
817
+ { signal: request.signal }
818
+ )) {
819
+ controller.enqueue(
820
+ encoder.encode(`event: ${event.type}
821
+ data: ${JSON.stringify(event)}
822
+
823
+ `)
824
+ );
825
+ }
826
+ } catch (err) {
827
+ const message = err instanceof Error ? err.message : String(err);
828
+ controller.enqueue(
829
+ encoder.encode(
830
+ `event: error
831
+ data: ${JSON.stringify({ type: "error", error: { code: "STREAM_ERROR", message } })}
832
+
833
+ `
834
+ )
835
+ );
836
+ } finally {
837
+ controller.close();
838
+ }
839
+ }
840
+ });
841
+ return new Response(readable, {
842
+ status: 200,
843
+ headers: {
844
+ "Content-Type": "text/event-stream",
845
+ "Cache-Control": "no-cache, no-transform",
846
+ Connection: "keep-alive"
847
+ }
848
+ });
849
+ }
850
+ const { status, response } = await processAiRequest(
851
+ engine,
852
+ body,
853
+ request.signal
854
+ );
855
+ return new Response(JSON.stringify(response), {
856
+ status,
857
+ headers: {
858
+ "Content-Type": "application/json"
859
+ }
860
+ });
861
+ } catch (err) {
862
+ const message = err instanceof Error ? err.message : String(err);
863
+ return new Response(
864
+ JSON.stringify({
865
+ success: false,
866
+ error: {
867
+ code: "INTERNAL_SERVER_ERROR",
868
+ message
869
+ }
870
+ }),
871
+ {
872
+ status: 500,
873
+ headers: {
874
+ "Content-Type": "application/json"
875
+ }
876
+ }
877
+ );
878
+ }
879
+ };
880
+ }
881
+ export {
882
+ AnthropicAdapter,
883
+ CustomHttpAdapter,
884
+ GeminiAdapter,
885
+ KubuildAiEngine,
886
+ OpenAiAdapter,
887
+ createAiHandler,
888
+ processAiRequest
889
+ };
890
+ //# sourceMappingURL=index.js.map