@kubuild/ai 0.3.1 → 0.5.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 (70) hide show
  1. package/dist/{chunk-27AY2KBI.js → chunk-3DRAVYU3.js} +173 -46
  2. package/dist/chunk-3DRAVYU3.js.map +1 -0
  3. package/dist/chunk-ILLNRABB.js +24 -0
  4. package/dist/chunk-ILLNRABB.js.map +1 -0
  5. package/dist/{chunk-JY4NHD3Q.js → chunk-K7CBTQCY.js} +91 -1
  6. package/dist/chunk-K7CBTQCY.js.map +1 -0
  7. package/dist/{chunk-UH24LHRY.js → chunk-YETTTWHI.js} +226 -1
  8. package/dist/chunk-YETTTWHI.js.map +1 -0
  9. package/dist/client/ai-client.d.ts +27 -1
  10. package/dist/client/ai-client.d.ts.map +1 -1
  11. package/dist/client/index.cjs +90 -0
  12. package/dist/client/index.cjs.map +1 -1
  13. package/dist/client/index.js +1 -1
  14. package/dist/core/document-outline.d.ts +60 -0
  15. package/dist/core/document-outline.d.ts.map +1 -0
  16. package/dist/core/index.d.ts +2 -0
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/messages.d.ts +18 -0
  19. package/dist/core/messages.d.ts.map +1 -0
  20. package/dist/core/prompt-compiler.d.ts +29 -0
  21. package/dist/core/prompt-compiler.d.ts.map +1 -1
  22. package/dist/index.cjs +261 -2
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.js +31 -3
  25. package/dist/react/index.cjs +233 -0
  26. package/dist/react/index.cjs.map +1 -1
  27. package/dist/react/index.d.ts +1 -0
  28. package/dist/react/index.d.ts.map +1 -1
  29. package/dist/react/index.js +143 -1
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/react/use-ai-agent.d.ts +47 -0
  32. package/dist/react/use-ai-agent.d.ts.map +1 -0
  33. package/dist/react/use-ai-generator.d.ts +2 -1
  34. package/dist/react/use-ai-generator.d.ts.map +1 -1
  35. package/dist/server/adapters/anthropic.d.ts.map +1 -1
  36. package/dist/server/adapters/custom.d.ts.map +1 -1
  37. package/dist/server/adapters/gemini.d.ts.map +1 -1
  38. package/dist/server/adapters/index.cjs +180 -45
  39. package/dist/server/adapters/index.cjs.map +1 -1
  40. package/dist/server/adapters/index.js +2 -1
  41. package/dist/server/adapters/openai.d.ts +25 -0
  42. package/dist/server/adapters/openai.d.ts.map +1 -1
  43. package/dist/server/agent.d.ts +59 -0
  44. package/dist/server/agent.d.ts.map +1 -0
  45. package/dist/server/engine.d.ts +5 -1
  46. package/dist/server/engine.d.ts.map +1 -1
  47. package/dist/server/handler.d.ts +18 -2
  48. package/dist/server/handler.d.ts.map +1 -1
  49. package/dist/server/index.cjs +1843 -258
  50. package/dist/server/index.cjs.map +1 -1
  51. package/dist/server/index.d.ts +2 -0
  52. package/dist/server/index.d.ts.map +1 -1
  53. package/dist/server/index.js +1379 -133
  54. package/dist/server/index.js.map +1 -1
  55. package/dist/server/tools/helpers.d.ts +66 -0
  56. package/dist/server/tools/helpers.d.ts.map +1 -0
  57. package/dist/server/tools/index.d.ts +25 -0
  58. package/dist/server/tools/index.d.ts.map +1 -0
  59. package/dist/server/tools/read-tools.d.ts +3 -0
  60. package/dist/server/tools/read-tools.d.ts.map +1 -0
  61. package/dist/server/tools/types.d.ts +51 -0
  62. package/dist/server/tools/types.d.ts.map +1 -0
  63. package/dist/server/tools/write-tools.d.ts +3 -0
  64. package/dist/server/tools/write-tools.d.ts.map +1 -0
  65. package/dist/types.d.ts +224 -2
  66. package/dist/types.d.ts.map +1 -1
  67. package/package.json +5 -5
  68. package/dist/chunk-27AY2KBI.js.map +0 -1
  69. package/dist/chunk-JY4NHD3Q.js.map +0 -1
  70. package/dist/chunk-UH24LHRY.js.map +0 -1
@@ -1,18 +1,27 @@
1
1
  import {
2
+ buildAgentSystemPrompt,
2
3
  buildJsonSchemaForMode,
4
+ buildSelectionContext,
3
5
  buildSystemPrompt,
4
6
  compileComponentCatalog,
5
7
  extractJsonFromResponse,
8
+ getNodeLabel,
6
9
  normalizeAndValidatePageDocument,
7
10
  normalizeAndValidateRefactoredNode,
8
- normalizeAndValidateSectionNode
9
- } from "../chunk-UH24LHRY.js";
11
+ normalizeAndValidateSectionNode,
12
+ normalizeNodeTree,
13
+ pruneNodeForPrompt,
14
+ summarizeNodeTree
15
+ } from "../chunk-YETTTWHI.js";
10
16
  import {
11
17
  AnthropicAdapter,
12
18
  CustomHttpAdapter,
13
19
  GeminiAdapter,
14
20
  OpenAiAdapter
15
- } from "../chunk-27AY2KBI.js";
21
+ } from "../chunk-3DRAVYU3.js";
22
+ import {
23
+ getMessageText
24
+ } from "../chunk-ILLNRABB.js";
16
25
 
17
26
  // src/server/engine.ts
18
27
  import { DocumentMetadataSchema } from "@kubuild/schema";
@@ -55,6 +64,13 @@ var KubuildAiEngine = class {
55
64
  Tone: ${request.tone}`;
56
65
  if (request.locale) userPrompt += `
57
66
  Language/Locale: ${request.locale}`;
67
+ if (request.conversationHistory && request.conversationHistory.length > 0) {
68
+ const chatContext = request.conversationHistory.map((m) => `${m.role.toUpperCase()}: ${getMessageText(m)}`).join("\n");
69
+ userPrompt += `
70
+
71
+ Prior Conversation Discussion Context:
72
+ ${chatContext}`;
73
+ }
58
74
  const jsonSchema = buildJsonSchemaForMode("full-page");
59
75
  const result = await this.options.adapter.generate({
60
76
  systemPrompt,
@@ -193,18 +209,18 @@ ${JSON.stringify(request.node, null, 2)}`;
193
209
  };
194
210
  }
195
211
  }
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 = `
212
+ buildPlanPrompts(request) {
213
+ let sectionGuidance = "Plan between 4 to 6 cohesive, essential sections (e.g., hero, features, testimonials, pricing, cta, footer) that fulfill the request thoroughly.";
214
+ if (typeof request.sectionCount === "number") {
215
+ sectionGuidance = `Plan exactly ${request.sectionCount} cohesive sections that fulfill the request.`;
216
+ } else if (request.sectionCount?.min || request.sectionCount?.max) {
217
+ const min = request.sectionCount.min ?? 3;
218
+ const max = request.sectionCount.max ?? 8;
219
+ sectionGuidance = `Plan between ${min} and ${max} cohesive sections that fulfill the request.`;
220
+ } else {
221
+ sectionGuidance += " If the user request mentions a specific number or list of sections, respect the user request.";
222
+ }
223
+ const planSystemPrompt = `
208
224
  You are a web architect for the KUBUILD page builder.
209
225
  Given the user's prompt, plan the website structure. Output pure JSON (no markdown fences, no explanatory text):
210
226
  {
@@ -226,44 +242,113 @@ Given the user's prompt, plan the website structure. Output pure JSON (no markdo
226
242
  }
227
243
  ]
228
244
  }
229
- Plan 3 cohesive, essential sections (e.g., hero, features, and cta) that fulfill the request.
245
+ ${sectionGuidance}
230
246
  `;
231
- let planUserPrompt = `User Request: ${request.prompt}`;
232
- if (request.stylePreference) {
233
- planUserPrompt += `
247
+ let planUserPrompt = `User Request: ${request.prompt}`;
248
+ if (request.stylePreference) {
249
+ planUserPrompt += `
234
250
  Style Preference: ${request.stylePreference}`;
235
- }
236
- if (request.tone) planUserPrompt += `
251
+ }
252
+ if (request.tone) planUserPrompt += `
237
253
  Tone: ${request.tone}`;
238
- if (request.locale) planUserPrompt += `
254
+ if (request.locale) planUserPrompt += `
239
255
  Locale: ${request.locale}`;
240
- this.log("info", "[SSE] Generating website layout plan...");
256
+ if (request.conversationHistory && request.conversationHistory.length > 0) {
257
+ const chatContext = request.conversationHistory.map((m) => `${m.role.toUpperCase()}: ${getMessageText(m)}`).join("\n");
258
+ planUserPrompt += `
259
+
260
+ Prior Conversation Discussion Context:
261
+ ${chatContext}`;
262
+ }
263
+ return { systemPrompt: planSystemPrompt, userPrompt: planUserPrompt };
264
+ }
265
+ async planPage(request, context) {
266
+ let rawText = "";
267
+ try {
268
+ const { systemPrompt, userPrompt } = this.buildPlanPrompts(request);
269
+ this.log("info", `Planning website layout for: "${request.prompt}"`);
241
270
  const planResult = await this.options.adapter.generate({
242
- systemPrompt: planSystemPrompt,
243
- userPrompt: planUserPrompt,
271
+ systemPrompt,
272
+ userPrompt,
244
273
  signal: context?.signal
245
274
  });
246
- this.log("debug", "[SSE] Raw plan response from model", planResult.text);
275
+ rawText = planResult.text;
276
+ const plan = extractJsonFromResponse(rawText);
277
+ return {
278
+ success: true,
279
+ data: plan,
280
+ usage: planResult.usage,
281
+ rawModelResponse: rawText
282
+ };
283
+ } catch (err) {
284
+ const message = err instanceof Error ? err.message : String(err);
285
+ this.log("warn", "Failed to generate plan JSON, using fallback plan", message);
286
+ const fallbackPlan = {
287
+ title: "AI Generated Page",
288
+ description: "Generated by KUBUILD AI",
289
+ sections: [
290
+ { type: "hero", title: "Hero Banner", prompt: `Hero section for: ${request.prompt}` },
291
+ { type: "features", title: "Key Features", prompt: `Features grid for: ${request.prompt}` },
292
+ { type: "testimonials", title: "Testimonials", prompt: `Social proof and customer reviews for: ${request.prompt}` },
293
+ { type: "cta", title: "Call To Action", prompt: `Call to action section for: ${request.prompt}` },
294
+ { type: "footer", title: "Footer", prompt: `Footer navigation and copyright for: ${request.prompt}` }
295
+ ]
296
+ };
297
+ return {
298
+ success: true,
299
+ data: fallbackPlan,
300
+ rawModelResponse: rawText || void 0
301
+ };
302
+ }
303
+ }
304
+ /**
305
+ * Progressive Section Streaming Generator.
306
+ * Emits structured SSE events: status -> metadata -> section (one by one) -> complete.
307
+ */
308
+ async *streamPage(request, context) {
309
+ try {
310
+ this.log("info", `[SSE] Starting streamPage for prompt: "${request.prompt}"`);
311
+ yield {
312
+ type: "status",
313
+ message: "Analyzing requirements and planning page sections..."
314
+ };
247
315
  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
- };
316
+ if (request.plan && Array.isArray(request.plan.sections) && request.plan.sections.length > 0) {
317
+ plan = request.plan;
318
+ this.log("info", `[SSE] Using approved pre-planned structure with ${plan.sections?.length ?? 0} sections`);
319
+ } else {
320
+ const { systemPrompt: planSystemPrompt, userPrompt: planUserPrompt } = this.buildPlanPrompts(request);
321
+ this.log("info", "[SSE] Generating website layout plan...");
322
+ const planResult = await this.options.adapter.generate({
323
+ systemPrompt: planSystemPrompt,
324
+ userPrompt: planUserPrompt,
325
+ signal: context?.signal
326
+ });
327
+ this.log("debug", "[SSE] Raw plan response from model", planResult.text);
328
+ try {
329
+ plan = extractJsonFromResponse(planResult.text);
330
+ this.log("debug", "[SSE] Parsed plan successfully", plan);
331
+ } catch (parseErr) {
332
+ this.log("warn", "[SSE] Failed to parse plan JSON, using fallback plan", parseErr);
333
+ plan = {
334
+ title: "AI Generated Page",
335
+ description: "Generated by KUBUILD AI",
336
+ sections: [
337
+ { type: "hero", title: "Hero Banner", prompt: `Hero section for: ${request.prompt}` },
338
+ { type: "features", title: "Key Features", prompt: `Features grid for: ${request.prompt}` },
339
+ { type: "testimonials", title: "Testimonials", prompt: `Social proof for: ${request.prompt}` },
340
+ { type: "cta", title: "Call To Action", prompt: `Call to action section for: ${request.prompt}` },
341
+ { type: "footer", title: "Footer", prompt: `Footer for: ${request.prompt}` }
342
+ ]
343
+ };
344
+ }
262
345
  }
263
346
  const sectionsToGenerate = Array.isArray(plan.sections) && plan.sections.length > 0 ? plan.sections : [
264
347
  { type: "hero", title: "Hero Section", prompt: `Hero banner for: ${request.prompt}` },
265
348
  { type: "features", title: "Features", prompt: `Features grid for: ${request.prompt}` },
266
- { type: "cta", title: "Call To Action", prompt: `CTA section for: ${request.prompt}` }
349
+ { type: "testimonials", title: "Testimonials", prompt: `Social proof for: ${request.prompt}` },
350
+ { type: "cta", title: "Call To Action", prompt: `CTA section for: ${request.prompt}` },
351
+ { type: "footer", title: "Footer", prompt: `Footer section for: ${request.prompt}` }
267
352
  ];
268
353
  const metadata = DocumentMetadataSchema.parse({
269
354
  title: plan.title || "AI Generated Page",
@@ -423,7 +508,7 @@ ${sectionSummary || "(Canvas is currently empty)"}
423
508
  Currently Selected Component Node ID: "${request.selectedNodeId}"`;
424
509
  }
425
510
  const lastUserMsg = [...request.messages].reverse().find((m) => m.role === "user");
426
- const userPrompt = lastUserMsg?.content || "Hello";
511
+ const userPrompt = lastUserMsg ? getMessageText(lastUserMsg) || "Hello" : "Hello";
427
512
  return { systemPrompt, userPrompt };
428
513
  }
429
514
  /**
@@ -564,8 +649,1067 @@ Currently Selected Component Node ID: "${request.selectedNodeId}"`;
564
649
  }
565
650
  };
566
651
 
652
+ // src/server/tools/helpers.ts
653
+ import { NodeSchema } from "@kubuild/schema";
654
+ import {
655
+ collectNodeIdSet,
656
+ findNodeById,
657
+ validateDocumentSecurity
658
+ } from "@kubuild/core";
659
+ function fail(summary, message, extra) {
660
+ return {
661
+ ok: false,
662
+ summary,
663
+ content: { ok: false, error: message, ...extra ?? {} }
664
+ };
665
+ }
666
+ function succeed(summary, content, rest) {
667
+ return { ok: true, summary, content: { ok: true, ...content }, ...rest ?? {} };
668
+ }
669
+ function readString(input, key) {
670
+ const value = input[key];
671
+ return typeof value === "string" && value.trim() ? value.trim() : null;
672
+ }
673
+ function readPlainObject(input, key) {
674
+ const value = input[key];
675
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
676
+ return value;
677
+ }
678
+ function readOptionalIndex(input, key) {
679
+ const value = input[key];
680
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
681
+ return void 0;
682
+ }
683
+ function similarity(a, b) {
684
+ if (a === b) return Infinity;
685
+ let shared = 0;
686
+ const max = Math.min(a.length, b.length);
687
+ while (shared < max && a[shared] === b[shared]) shared++;
688
+ return shared + (a.includes(b) || b.includes(a) ? 3 : 0);
689
+ }
690
+ function suggestNodeIds(document, badId, limit = 5) {
691
+ return Array.from(collectNodeIdSet(document.document)).map((id) => ({ id, score: similarity(id.toLowerCase(), badId.toLowerCase()) })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((entry) => entry.id);
692
+ }
693
+ function resolveNode(document, nodeId, toolName) {
694
+ const node = findNodeById(document.document, nodeId);
695
+ if (!node) {
696
+ return {
697
+ error: fail(
698
+ `${toolName}: node "${nodeId}" tidak ditemukan`,
699
+ `No node with id "${nodeId}" exists in this page. Node ids must come from the outline or from a tool result \u2014 never invented.`,
700
+ { suggestedNodeIds: suggestNodeIds(document, nodeId) }
701
+ )
702
+ };
703
+ }
704
+ return { node };
705
+ }
706
+ function findCatalogEntry(catalog, type) {
707
+ return catalog.find((entry) => entry.type === type);
708
+ }
709
+ function checkNesting(catalog, parent, childType) {
710
+ if (catalog.length === 0) return null;
711
+ const parentSpec = findCatalogEntry(catalog, parent.type);
712
+ if (parentSpec && !parentSpec.acceptsChildren) {
713
+ return `Component "${parent.type}" (#${parent.id}) cannot contain children. Insert into a container/section node instead.`;
714
+ }
715
+ if (parentSpec?.allowedChildren && parentSpec.allowedChildren.length > 0) {
716
+ if (!parentSpec.allowedChildren.includes(childType)) {
717
+ return `Component "${parent.type}" only accepts children of type [${parentSpec.allowedChildren.join(", ")}] \u2014 "${childType}" is not allowed.`;
718
+ }
719
+ }
720
+ const childSpec = findCatalogEntry(catalog, childType);
721
+ if (childSpec?.disallowedParents?.includes(parent.type)) {
722
+ return `Component "${childType}" cannot be placed inside "${parent.type}".`;
723
+ }
724
+ return null;
725
+ }
726
+ function checkComponentType(catalog, type) {
727
+ if (catalog.length === 0) return null;
728
+ if (findCatalogEntry(catalog, type)) return null;
729
+ const known = catalog.map((entry) => entry.type);
730
+ return `Unknown component type "${type}". Use list_component_types to see valid types. Known types: ${known.slice(0, 40).join(", ")}${known.length > 40 ? ", \u2026" : ""}`;
731
+ }
732
+ function checkDocumentSecurity(document, limits) {
733
+ const result = validateDocumentSecurity(document, limits);
734
+ if (result.safe) return null;
735
+ return result.errors.map((error) => `${error.code}: ${error.message}`).join("; ");
736
+ }
737
+ function normalizeIncomingNode(raw, document, options = {}) {
738
+ let normalized;
739
+ try {
740
+ normalized = normalizeNodeTree(raw);
741
+ } catch (err) {
742
+ return { error: err instanceof Error ? err.message : String(err) };
743
+ }
744
+ if (!options.preserveIds) {
745
+ const taken = collectNodeIdSet(document.document);
746
+ const reassign = (node) => {
747
+ if (!node.id || taken.has(node.id)) {
748
+ let counter = 1;
749
+ let candidate = `${node.type}-${counter}`;
750
+ while (taken.has(candidate)) {
751
+ counter++;
752
+ candidate = `${node.type}-${counter}`;
753
+ }
754
+ node.id = candidate;
755
+ }
756
+ taken.add(node.id);
757
+ for (const child of node.children ?? []) reassign(child);
758
+ };
759
+ reassign(normalized);
760
+ }
761
+ const parsed = NodeSchema.safeParse(normalized);
762
+ if (!parsed.success) {
763
+ return {
764
+ error: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
765
+ };
766
+ }
767
+ return { node: parsed.data };
768
+ }
769
+ function describeNode(node) {
770
+ const label = getNodeLabel(node, 32);
771
+ return label ? `${node.type} "${label}" (#${node.id})` : `${node.type} (#${node.id})`;
772
+ }
773
+
774
+ // src/server/tools/read-tools.ts
775
+ var getPageOutline = {
776
+ kind: "read",
777
+ definition: {
778
+ name: "get_page_outline",
779
+ description: "Return the page structure as an indented outline of node ids, types, labels and child counts. Use this to re-orient after edits. Does not include props or styles \u2014 call read_node for those.",
780
+ inputSchema: {
781
+ type: "object",
782
+ properties: {
783
+ maxDepth: {
784
+ type: "integer",
785
+ description: "How deep to expand the tree before collapsing. Default 3."
786
+ }
787
+ }
788
+ }
789
+ },
790
+ execute(input, context) {
791
+ const maxDepth = typeof input.maxDepth === "number" && input.maxDepth > 0 ? Math.floor(input.maxDepth) : 3;
792
+ return succeed("Membaca struktur halaman", {
793
+ outline: summarizeNodeTree(context.document.document, { maxDepth })
794
+ });
795
+ }
796
+ };
797
+ var readNode = {
798
+ kind: "read",
799
+ definition: {
800
+ name: "read_node",
801
+ description: "Return one node with its full props and styles, plus its parent, position and siblings. Always read a node before editing it, so you change only the fields that need changing.",
802
+ inputSchema: {
803
+ type: "object",
804
+ properties: {
805
+ nodeId: { type: "string", description: "Id of the node to read (from the outline)." },
806
+ includeChildren: {
807
+ type: "boolean",
808
+ description: "Include the node subtree. Default false \u2014 keeps the result small."
809
+ }
810
+ },
811
+ required: ["nodeId"]
812
+ }
813
+ },
814
+ execute(input, context) {
815
+ const nodeId = readString(input, "nodeId");
816
+ if (!nodeId) return fail("read_node: nodeId kosong", 'Argument "nodeId" is required.');
817
+ const resolved = resolveNode(context.document, nodeId, "read_node");
818
+ if ("error" in resolved) return resolved.error;
819
+ const selection = buildSelectionContext(context.document.document, nodeId);
820
+ const includeChildren = input.includeChildren === true;
821
+ const node = includeChildren ? pruneNodeForPrompt(resolved.node, 3) : { ...resolved.node, children: void 0 };
822
+ return succeed(`Membaca node ${nodeId}`, {
823
+ node,
824
+ childCount: resolved.node.children?.length ?? 0,
825
+ parentId: selection?.parentId ?? null,
826
+ index: selection?.index ?? 0,
827
+ ancestors: selection?.ancestors.map((a) => ({ id: a.id, type: a.type })) ?? [],
828
+ siblings: selection?.siblings.map((sibling, i) => ({
829
+ index: i,
830
+ id: sibling.id,
831
+ type: sibling.type,
832
+ label: getNodeLabel(sibling)
833
+ })) ?? []
834
+ });
835
+ }
836
+ };
837
+ var findNodes = {
838
+ kind: "read",
839
+ definition: {
840
+ name: "find_nodes",
841
+ description: 'Search the page for nodes by component type and/or visible text. Use this instead of guessing ids when the user refers to something by what it says ("the Get Started button").',
842
+ inputSchema: {
843
+ type: "object",
844
+ properties: {
845
+ type: { type: "string", description: 'Component type to match exactly, e.g. "button".' },
846
+ textContains: {
847
+ type: "string",
848
+ description: "Case-insensitive substring to match against the node text/label props."
849
+ },
850
+ parentId: { type: "string", description: "Limit the search to this node subtree." },
851
+ limit: { type: "integer", description: "Max results. Default 20." }
852
+ }
853
+ }
854
+ },
855
+ execute(input, context) {
856
+ const type = readString(input, "type");
857
+ const textContains = readString(input, "textContains");
858
+ const parentId = readString(input, "parentId");
859
+ const limit = typeof input.limit === "number" && input.limit > 0 ? Math.floor(input.limit) : 20;
860
+ if (!type && !textContains) {
861
+ return fail(
862
+ "find_nodes: filter kosong",
863
+ 'Provide at least one of "type" or "textContains" \u2014 an unfiltered search would just return the whole outline.'
864
+ );
865
+ }
866
+ let root = context.document.document;
867
+ if (parentId) {
868
+ const resolved = resolveNode(context.document, parentId, "find_nodes");
869
+ if ("error" in resolved) return resolved.error;
870
+ root = resolved.node;
871
+ }
872
+ const needle = textContains?.toLowerCase();
873
+ const matches = [];
874
+ const walk = (node) => {
875
+ if (matches.length >= limit) return;
876
+ const label = getNodeLabel(node);
877
+ const typeMatches = !type || node.type === type;
878
+ const textMatches = !needle || label.toLowerCase().includes(needle);
879
+ if (typeMatches && textMatches) {
880
+ matches.push({ id: node.id, type: node.type, label });
881
+ }
882
+ for (const child of node.children ?? []) walk(child);
883
+ };
884
+ walk(root);
885
+ return succeed(
886
+ `Mencari node (${matches.length} hasil)`,
887
+ matches.length > 0 ? { matches, count: matches.length } : {
888
+ matches: [],
889
+ count: 0,
890
+ hint: "Nothing matched. Try a broader filter, or call get_page_outline to see what exists."
891
+ }
892
+ );
893
+ }
894
+ };
895
+ var listComponentTypes = {
896
+ kind: "read",
897
+ definition: {
898
+ name: "list_component_types",
899
+ description: "List component types available in this editor, with their props and nesting rules. Call this before inserting a component type you have not used yet.",
900
+ inputSchema: {
901
+ type: "object",
902
+ properties: {
903
+ category: { type: "string", description: 'Filter to one category, e.g. "layout".' }
904
+ }
905
+ }
906
+ },
907
+ execute(input, context) {
908
+ const category = readString(input, "category");
909
+ const entries = category ? context.catalog.filter((spec) => spec.category === category) : context.catalog;
910
+ return succeed(`Melihat katalog komponen (${entries.length})`, {
911
+ components: entries.map((spec) => ({
912
+ type: spec.type,
913
+ label: spec.label,
914
+ category: spec.category,
915
+ acceptsChildren: spec.acceptsChildren,
916
+ allowedChildren: spec.allowedChildren,
917
+ props: spec.props?.map((prop) => ({
918
+ name: prop.name,
919
+ type: prop.type,
920
+ options: prop.options
921
+ }))
922
+ }))
923
+ });
924
+ }
925
+ };
926
+ var READ_TOOLS = [getPageOutline, readNode, findNodes, listComponentTypes];
927
+
928
+ // src/server/tools/write-tools.ts
929
+ import {
930
+ duplicateNode,
931
+ findNodeById as findNodeById2,
932
+ insertNode,
933
+ moveNode,
934
+ removeNode,
935
+ replaceNode,
936
+ updateProps,
937
+ updateStyle
938
+ } from "@kubuild/core";
939
+ var BREAKPOINTS = ["base", "desktop", "tablet", "mobile"];
940
+ function applyCommand(toolName, run) {
941
+ try {
942
+ return run();
943
+ } catch (err) {
944
+ const message = err instanceof Error ? err.message : String(err);
945
+ return { error: fail(`${toolName} gagal`, message) };
946
+ }
947
+ }
948
+ function guardSecurity(toolName, document, context) {
949
+ const violation = checkDocumentSecurity(document, context.securityLimits);
950
+ if (!violation) return null;
951
+ return fail(
952
+ `${toolName} ditolak (security)`,
953
+ `The resulting document failed the security check and was rejected: ${violation}`
954
+ );
955
+ }
956
+ var updateNodeProps = {
957
+ kind: "write",
958
+ definition: {
959
+ name: "update_node_props",
960
+ description: "Change the props of one existing node (text, label, href, src, level, \u2026). This is the right tool for any content/copy change \u2014 it touches nothing else on the page.",
961
+ inputSchema: {
962
+ type: "object",
963
+ properties: {
964
+ nodeId: { type: "string", description: "Id of the node to edit." },
965
+ props: {
966
+ type: "object",
967
+ description: "Props to write. Include only the props that should change."
968
+ },
969
+ merge: {
970
+ type: "boolean",
971
+ description: "true (default) merges with existing props. Only pass false to deliberately clear the other props."
972
+ }
973
+ },
974
+ required: ["nodeId", "props"]
975
+ }
976
+ },
977
+ execute(input, context) {
978
+ const nodeId = readString(input, "nodeId");
979
+ if (!nodeId) return fail("update_node_props: nodeId kosong", 'Argument "nodeId" is required.');
980
+ const props = readPlainObject(input, "props");
981
+ if (!props) {
982
+ return fail(
983
+ "update_node_props: props tidak valid",
984
+ 'Argument "props" must be a JSON object of prop names to values.'
985
+ );
986
+ }
987
+ const resolved = resolveNode(context.document, nodeId, "update_node_props");
988
+ if ("error" in resolved) return resolved.error;
989
+ const merge = input.merge !== false;
990
+ const applied = applyCommand(
991
+ "update_node_props",
992
+ () => updateProps(context.document, { nodeId, props, merge })
993
+ );
994
+ if ("error" in applied) return applied.error;
995
+ const violation = guardSecurity("update_node_props", applied.document, context);
996
+ if (violation) return violation;
997
+ const op = { kind: "update-props", nodeId, props, merge };
998
+ const updated = findNodeById2(applied.document.document, nodeId);
999
+ return succeed(
1000
+ `Ubah props ${describeNode(resolved.node)}`,
1001
+ { nodeId, props: updated?.props ?? props, changedKeys: Object.keys(props) },
1002
+ { op, document: applied.document }
1003
+ );
1004
+ }
1005
+ };
1006
+ var updateNodeStyles = {
1007
+ kind: "write",
1008
+ definition: {
1009
+ name: "update_node_styles",
1010
+ description: 'Change the styles of one existing node. Use `breakpoint` for normal styling and `state` for pseudo-classes like ":hover". This is the right tool for any color/spacing/size/typography change.',
1011
+ inputSchema: {
1012
+ type: "object",
1013
+ properties: {
1014
+ nodeId: { type: "string", description: "Id of the node to restyle." },
1015
+ styles: {
1016
+ type: "object",
1017
+ description: 'CSS properties in camelCase with primitive values, e.g. { "backgroundColor": "#16a34a", "paddingTop": "24px" }. Never nest objects here.'
1018
+ },
1019
+ breakpoint: {
1020
+ type: "string",
1021
+ enum: ["base", "desktop", "tablet", "mobile"],
1022
+ description: 'Breakpoint layer to write. Default "base".'
1023
+ },
1024
+ state: {
1025
+ type: "string",
1026
+ description: 'Pseudo-state layer such as ":hover", ":focus", ":active". Mutually exclusive with breakpoint.'
1027
+ },
1028
+ merge: {
1029
+ type: "boolean",
1030
+ description: "true (default) merges with existing styles in that layer."
1031
+ }
1032
+ },
1033
+ required: ["nodeId", "styles"]
1034
+ }
1035
+ },
1036
+ execute(input, context) {
1037
+ const nodeId = readString(input, "nodeId");
1038
+ if (!nodeId) return fail("update_node_styles: nodeId kosong", 'Argument "nodeId" is required.');
1039
+ const styles = readPlainObject(input, "styles");
1040
+ if (!styles) {
1041
+ return fail(
1042
+ "update_node_styles: styles tidak valid",
1043
+ 'Argument "styles" must be a JSON object of CSS properties with primitive values.'
1044
+ );
1045
+ }
1046
+ const nested = Object.entries(styles).find(
1047
+ ([, value]) => value !== null && typeof value === "object"
1048
+ );
1049
+ if (nested) {
1050
+ return fail(
1051
+ "update_node_styles: nilai bersarang",
1052
+ `Style property "${nested[0]}" has an object value. Style values must be primitives \u2014 use the "state" argument for pseudo-classes instead of nesting them.`
1053
+ );
1054
+ }
1055
+ const rawState = readString(input, "state");
1056
+ const rawBreakpoint = readString(input, "breakpoint");
1057
+ if (rawState && rawBreakpoint) {
1058
+ return fail(
1059
+ "update_node_styles: argumen bentrok",
1060
+ 'Pass either "breakpoint" or "state", not both \u2014 a pseudo-state layer is not per-breakpoint.'
1061
+ );
1062
+ }
1063
+ if (rawBreakpoint && !BREAKPOINTS.includes(rawBreakpoint)) {
1064
+ return fail(
1065
+ "update_node_styles: breakpoint tidak dikenal",
1066
+ `Unknown breakpoint "${rawBreakpoint}". Valid values: ${BREAKPOINTS.join(", ")}.`
1067
+ );
1068
+ }
1069
+ const resolved = resolveNode(context.document, nodeId, "update_node_styles");
1070
+ if ("error" in resolved) return resolved.error;
1071
+ const state = rawState ? rawState.startsWith(":") ? rawState : `:${rawState}` : void 0;
1072
+ const breakpoint = state ? void 0 : rawBreakpoint ?? "base";
1073
+ const merge = input.merge !== false;
1074
+ const applied = applyCommand(
1075
+ "update_node_styles",
1076
+ () => updateStyle(context.document, { nodeId, styles, breakpoint, state, merge })
1077
+ );
1078
+ if ("error" in applied) return applied.error;
1079
+ const violation = guardSecurity("update_node_styles", applied.document, context);
1080
+ if (violation) return violation;
1081
+ const op = { kind: "update-styles", nodeId, styles, breakpoint, state, merge };
1082
+ const layer = state ? `state ${state}` : `breakpoint ${breakpoint}`;
1083
+ return succeed(
1084
+ `Ubah style ${describeNode(resolved.node)} (${layer})`,
1085
+ { nodeId, layer, appliedStyles: styles },
1086
+ { op, document: applied.document }
1087
+ );
1088
+ }
1089
+ };
1090
+ var insertComponent = {
1091
+ kind: "write",
1092
+ definition: {
1093
+ name: "insert_component",
1094
+ description: "Insert one new component into an existing parent node. Use this for adding a single element (a button, a paragraph, an image). For a whole new page section, use insert_section instead.",
1095
+ inputSchema: {
1096
+ type: "object",
1097
+ properties: {
1098
+ parentId: { type: "string", description: "Id of the node that will contain the new component." },
1099
+ type: { type: "string", description: "Component type from the catalog." },
1100
+ index: {
1101
+ type: "integer",
1102
+ description: "Position among the parent children. Omit to append at the end."
1103
+ },
1104
+ props: { type: "object", description: "Initial props for the new component." },
1105
+ styles: {
1106
+ type: "object",
1107
+ description: 'Initial styles, e.g. { "base": { "marginTop": "16px" } }.'
1108
+ },
1109
+ children: {
1110
+ type: "array",
1111
+ description: "Optional nested child nodes, each { type, props?, styles?, children? }.",
1112
+ items: { type: "object" }
1113
+ }
1114
+ },
1115
+ required: ["parentId", "type"]
1116
+ }
1117
+ },
1118
+ execute(input, context) {
1119
+ const parentId = readString(input, "parentId");
1120
+ const type = readString(input, "type");
1121
+ if (!parentId || !type) {
1122
+ return fail(
1123
+ "insert_component: argumen kurang",
1124
+ 'Arguments "parentId" and "type" are both required.'
1125
+ );
1126
+ }
1127
+ const typeError = checkComponentType(context.catalog, type);
1128
+ if (typeError) return fail("insert_component: tipe tidak dikenal", typeError);
1129
+ const resolvedParent = resolveNode(context.document, parentId, "insert_component");
1130
+ if ("error" in resolvedParent) return resolvedParent.error;
1131
+ const nestingError = checkNesting(context.catalog, resolvedParent.node, type);
1132
+ if (nestingError) return fail("insert_component: nesting tidak valid", nestingError);
1133
+ const normalized = normalizeIncomingNode(
1134
+ {
1135
+ type,
1136
+ props: readPlainObject(input, "props") ?? void 0,
1137
+ styles: readPlainObject(input, "styles") ?? void 0,
1138
+ children: Array.isArray(input.children) ? input.children : void 0
1139
+ },
1140
+ context.document
1141
+ );
1142
+ if ("error" in normalized) {
1143
+ return fail("insert_component: node tidak valid", normalized.error);
1144
+ }
1145
+ const index = readOptionalIndex(input, "index");
1146
+ const applied = applyCommand(
1147
+ "insert_component",
1148
+ () => insertNode(context.document, { parentId, node: normalized.node, index })
1149
+ );
1150
+ if ("error" in applied) return applied.error;
1151
+ const violation = guardSecurity("insert_component", applied.document, context);
1152
+ if (violation) return violation;
1153
+ const op = { kind: "insert-node", parentId, index, node: normalized.node };
1154
+ return succeed(
1155
+ `Tambah ${describeNode(normalized.node)} ke #${parentId}`,
1156
+ { nodeId: normalized.node.id, parentId, index: index ?? null },
1157
+ { op, document: applied.document }
1158
+ );
1159
+ }
1160
+ };
1161
+ var insertSection = {
1162
+ kind: "write",
1163
+ definition: {
1164
+ name: "insert_section",
1165
+ description: "Generate and insert a complete new page section (hero, features, pricing, FAQ, CTA, \u2026) from a description. Use this when the user asks to ADD a section \u2014 do not hand-build sections with insert_component.",
1166
+ inputSchema: {
1167
+ type: "object",
1168
+ properties: {
1169
+ prompt: {
1170
+ type: "string",
1171
+ description: "Detailed description of the section: purpose, content, layout and tone. Write it in the language the page uses."
1172
+ },
1173
+ index: {
1174
+ type: "integer",
1175
+ description: "Position among the page sections. Omit to append at the end."
1176
+ }
1177
+ },
1178
+ required: ["prompt"]
1179
+ }
1180
+ },
1181
+ async execute(input, context) {
1182
+ const prompt = readString(input, "prompt");
1183
+ if (!prompt) return fail("insert_section: prompt kosong", 'Argument "prompt" is required.');
1184
+ if (!context.generateSection) {
1185
+ return fail(
1186
+ "insert_section: tidak tersedia",
1187
+ "Section generation is not available in this deployment. Build the section with insert_component instead."
1188
+ );
1189
+ }
1190
+ const rootId = context.document.document.id;
1191
+ const existingSections = (context.document.document.children ?? []).map((child) => `${child.type}: ${getNodeLabel(child) || child.id}`).join(", ");
1192
+ let generated;
1193
+ try {
1194
+ generated = await context.generateSection({
1195
+ prompt,
1196
+ parentContext: `Page "${context.document.metadata?.title ?? "Untitled"}". Existing sections: ${existingSections || "(none)"}`
1197
+ });
1198
+ } catch (err) {
1199
+ return fail(
1200
+ "insert_section gagal",
1201
+ `Section generation failed: ${err instanceof Error ? err.message : String(err)}`
1202
+ );
1203
+ }
1204
+ const normalized = normalizeIncomingNode(generated, context.document);
1205
+ if ("error" in normalized) {
1206
+ return fail("insert_section: section tidak valid", normalized.error);
1207
+ }
1208
+ const index = readOptionalIndex(input, "index");
1209
+ const applied = applyCommand(
1210
+ "insert_section",
1211
+ () => insertNode(context.document, { parentId: rootId, node: normalized.node, index })
1212
+ );
1213
+ if ("error" in applied) return applied.error;
1214
+ const violation = guardSecurity("insert_section", applied.document, context);
1215
+ if (violation) return violation;
1216
+ const op = { kind: "insert-node", parentId: rootId, index, node: normalized.node };
1217
+ return succeed(
1218
+ `Tambah section baru (#${normalized.node.id})`,
1219
+ {
1220
+ nodeId: normalized.node.id,
1221
+ index: index ?? null,
1222
+ childCount: normalized.node.children?.length ?? 0
1223
+ },
1224
+ { op, document: applied.document }
1225
+ );
1226
+ }
1227
+ };
1228
+ var moveNodeTool = {
1229
+ kind: "write",
1230
+ definition: {
1231
+ name: "move_node",
1232
+ description: "Move an existing node to a different parent and/or position.",
1233
+ inputSchema: {
1234
+ type: "object",
1235
+ properties: {
1236
+ nodeId: { type: "string" },
1237
+ targetParentId: { type: "string", description: "Id of the new parent node." },
1238
+ index: { type: "integer", description: "Position within the new parent. Omit to append." }
1239
+ },
1240
+ required: ["nodeId", "targetParentId"]
1241
+ }
1242
+ },
1243
+ execute(input, context) {
1244
+ const nodeId = readString(input, "nodeId");
1245
+ const targetParentId = readString(input, "targetParentId");
1246
+ if (!nodeId || !targetParentId) {
1247
+ return fail(
1248
+ "move_node: argumen kurang",
1249
+ 'Arguments "nodeId" and "targetParentId" are both required.'
1250
+ );
1251
+ }
1252
+ const resolved = resolveNode(context.document, nodeId, "move_node");
1253
+ if ("error" in resolved) return resolved.error;
1254
+ const resolvedParent = resolveNode(context.document, targetParentId, "move_node");
1255
+ if ("error" in resolvedParent) return resolvedParent.error;
1256
+ const nestingError = checkNesting(context.catalog, resolvedParent.node, resolved.node.type);
1257
+ if (nestingError) return fail("move_node: nesting tidak valid", nestingError);
1258
+ const index = readOptionalIndex(input, "index");
1259
+ const applied = applyCommand(
1260
+ "move_node",
1261
+ () => moveNode(context.document, { nodeId, targetParentId, index })
1262
+ );
1263
+ if ("error" in applied) return applied.error;
1264
+ const violation = guardSecurity("move_node", applied.document, context);
1265
+ if (violation) return violation;
1266
+ const op = { kind: "move-node", nodeId, targetParentId, index };
1267
+ return succeed(
1268
+ `Pindah ${describeNode(resolved.node)} ke #${targetParentId}`,
1269
+ { nodeId, targetParentId, index: index ?? null },
1270
+ { op, document: applied.document }
1271
+ );
1272
+ }
1273
+ };
1274
+ var duplicateNodeTool = {
1275
+ kind: "write",
1276
+ definition: {
1277
+ name: "duplicate_node",
1278
+ description: "Duplicate an existing node and its subtree, with fresh ids. Useful for repeating an existing card/feature rather than authoring a new one from scratch.",
1279
+ inputSchema: {
1280
+ type: "object",
1281
+ properties: {
1282
+ nodeId: { type: "string" },
1283
+ targetParentId: {
1284
+ type: "string",
1285
+ description: "Where to put the copy. Defaults to the original node parent."
1286
+ },
1287
+ index: { type: "integer", description: "Position of the copy. Defaults to right after the original." }
1288
+ },
1289
+ required: ["nodeId"]
1290
+ }
1291
+ },
1292
+ execute(input, context) {
1293
+ const nodeId = readString(input, "nodeId");
1294
+ if (!nodeId) return fail("duplicate_node: nodeId kosong", 'Argument "nodeId" is required.');
1295
+ const resolved = resolveNode(context.document, nodeId, "duplicate_node");
1296
+ if ("error" in resolved) return resolved.error;
1297
+ const targetParentId = readString(input, "targetParentId") ?? void 0;
1298
+ if (targetParentId) {
1299
+ const resolvedParent = resolveNode(context.document, targetParentId, "duplicate_node");
1300
+ if ("error" in resolvedParent) return resolvedParent.error;
1301
+ }
1302
+ const index = readOptionalIndex(input, "index");
1303
+ const applied = applyCommand(
1304
+ "duplicate_node",
1305
+ () => duplicateNode(context.document, { nodeId, targetParentId, index })
1306
+ );
1307
+ if ("error" in applied) return applied.error;
1308
+ const violation = guardSecurity("duplicate_node", applied.document, context);
1309
+ if (violation) return violation;
1310
+ const op = { kind: "duplicate-node", nodeId, targetParentId, index };
1311
+ return succeed(
1312
+ `Duplikat ${describeNode(resolved.node)}`,
1313
+ { nodeId, targetParentId: targetParentId ?? null, index: index ?? null },
1314
+ { op, document: applied.document }
1315
+ );
1316
+ }
1317
+ };
1318
+ var deleteNode = {
1319
+ kind: "write",
1320
+ destructive: true,
1321
+ definition: {
1322
+ name: "delete_node",
1323
+ description: "Permanently remove a node and everything inside it. Only call this when the user unambiguously asked to remove that specific thing. If the request is vague, ask instead.",
1324
+ inputSchema: {
1325
+ type: "object",
1326
+ properties: {
1327
+ nodeId: { type: "string" },
1328
+ reason: {
1329
+ type: "string",
1330
+ description: "Short justification quoting what the user asked for. Shown to the user before they apply."
1331
+ }
1332
+ },
1333
+ required: ["nodeId", "reason"]
1334
+ }
1335
+ },
1336
+ execute(input, context) {
1337
+ const nodeId = readString(input, "nodeId");
1338
+ if (!nodeId) return fail("delete_node: nodeId kosong", 'Argument "nodeId" is required.');
1339
+ const reason = readString(input, "reason");
1340
+ if (!reason) {
1341
+ return fail(
1342
+ "delete_node: alasan kosong",
1343
+ 'Argument "reason" is required for destructive actions \u2014 state what the user asked for.'
1344
+ );
1345
+ }
1346
+ const resolved = resolveNode(context.document, nodeId, "delete_node");
1347
+ if ("error" in resolved) return resolved.error;
1348
+ const applied = applyCommand("delete_node", () => removeNode(context.document, { nodeId }));
1349
+ if ("error" in applied) return applied.error;
1350
+ const op = { kind: "delete-node", nodeId };
1351
+ return succeed(
1352
+ `Hapus ${describeNode(resolved.node)} \u2014 ${reason}`,
1353
+ { nodeId, removedType: resolved.node.type },
1354
+ { op, document: applied.document }
1355
+ );
1356
+ }
1357
+ };
1358
+ var replaceNodeTool = {
1359
+ kind: "write",
1360
+ destructive: true,
1361
+ definition: {
1362
+ name: "replace_node",
1363
+ description: "Replace a node subtree wholesale. LAST RESORT \u2014 only when the children genuinely must be restructured. Never use it to change props or styles; use update_node_props / update_node_styles for that.",
1364
+ inputSchema: {
1365
+ type: "object",
1366
+ properties: {
1367
+ nodeId: { type: "string", description: "Id of the node being replaced. The replacement keeps this id." },
1368
+ node: {
1369
+ type: "object",
1370
+ description: "Replacement node { type, props?, styles?, children? }. Its id is forced to nodeId."
1371
+ },
1372
+ reason: {
1373
+ type: "string",
1374
+ description: "Why a structural replacement is required instead of a props/styles edit."
1375
+ }
1376
+ },
1377
+ required: ["nodeId", "node", "reason"]
1378
+ }
1379
+ },
1380
+ execute(input, context) {
1381
+ const nodeId = readString(input, "nodeId");
1382
+ const raw = readPlainObject(input, "node");
1383
+ const reason = readString(input, "reason");
1384
+ if (!nodeId || !raw || !reason) {
1385
+ return fail(
1386
+ "replace_node: argumen kurang",
1387
+ 'Arguments "nodeId", "node" and "reason" are all required.'
1388
+ );
1389
+ }
1390
+ const resolved = resolveNode(context.document, nodeId, "replace_node");
1391
+ if ("error" in resolved) return resolved.error;
1392
+ const normalized = normalizeIncomingNode({ ...raw, id: void 0 }, context.document);
1393
+ if ("error" in normalized) {
1394
+ return fail("replace_node: node tidak valid", normalized.error);
1395
+ }
1396
+ const replacement = { ...normalized.node, id: nodeId, type: resolved.node.type };
1397
+ const applied = applyCommand(
1398
+ "replace_node",
1399
+ () => replaceNode(context.document, { nodeId, node: replacement })
1400
+ );
1401
+ if ("error" in applied) return applied.error;
1402
+ const violation = guardSecurity("replace_node", applied.document, context);
1403
+ if (violation) return violation;
1404
+ const op = { kind: "replace-node", nodeId, node: replacement };
1405
+ return succeed(
1406
+ `Ganti struktur ${describeNode(resolved.node)} \u2014 ${reason}`,
1407
+ { nodeId, childCount: replacement.children?.length ?? 0 },
1408
+ { op, document: applied.document }
1409
+ );
1410
+ }
1411
+ };
1412
+ var WRITE_TOOLS = [
1413
+ updateNodeProps,
1414
+ updateNodeStyles,
1415
+ insertComponent,
1416
+ insertSection,
1417
+ moveNodeTool,
1418
+ duplicateNodeTool,
1419
+ deleteNode,
1420
+ replaceNodeTool
1421
+ ];
1422
+
1423
+ // src/server/tools/index.ts
1424
+ function createDocumentTools(options = {}) {
1425
+ const { allowWrites = true, exclude = [] } = options;
1426
+ const tools = allowWrites ? [...READ_TOOLS, ...WRITE_TOOLS] : [...READ_TOOLS];
1427
+ return tools.filter((tool) => !exclude.includes(tool.definition.name));
1428
+ }
1429
+ function toToolDefinitions(tools) {
1430
+ return tools.map((tool) => tool.definition);
1431
+ }
1432
+
1433
+ // src/server/agent.ts
1434
+ var DEFAULT_MAX_STEPS = 8;
1435
+ var MAX_TOOL_CALLS_PER_STEP = 8;
1436
+ function toolResultMessage(blocks) {
1437
+ return { role: "user", content: blocks, timestamp: Date.now() };
1438
+ }
1439
+ var KubuildAiAgent = class {
1440
+ options;
1441
+ catalog;
1442
+ tools;
1443
+ constructor(options) {
1444
+ if (!options.adapter) {
1445
+ throw new Error("KubuildAiAgent requires an adapter instance");
1446
+ }
1447
+ if (!options.adapter.supportsTools) {
1448
+ throw new Error(
1449
+ `Adapter "${options.adapter.name}" does not support tool calling, which agent mode requires. Use an adapter with supportsTools === true (e.g. OpenAiAdapter).`
1450
+ );
1451
+ }
1452
+ this.options = options;
1453
+ this.catalog = compileComponentCatalog(options.registry);
1454
+ this.tools = options.tools ?? createDocumentTools();
1455
+ }
1456
+ get adapterName() {
1457
+ return this.options.adapter.name;
1458
+ }
1459
+ refreshCatalog() {
1460
+ this.catalog = compileComponentCatalog(this.options.registry);
1461
+ }
1462
+ log(level, message, meta) {
1463
+ if (this.options.logger) {
1464
+ this.options.logger(level, message, meta);
1465
+ } else if (this.options.debug) {
1466
+ console.log(`[KUBUILD-AGENT ${level.toUpperCase()}] ${message}`, meta !== void 0 ? meta : "");
1467
+ }
1468
+ }
1469
+ findTool(name) {
1470
+ return this.tools.find((tool) => tool.definition.name === name);
1471
+ }
1472
+ /**
1473
+ * Runs one tool call against the current snapshot.
1474
+ *
1475
+ * An unknown tool name is answered with a normal (failed) tool result rather than an
1476
+ * exception, for the same reason bad arguments are: it gives the model a chance to pick a
1477
+ * real tool on the next step instead of killing the run.
1478
+ */
1479
+ async executeToolCall(call, snapshot, signal) {
1480
+ const tool = this.findTool(call.name);
1481
+ if (!tool) {
1482
+ const available = this.tools.map((t) => t.definition.name).join(", ");
1483
+ return {
1484
+ ok: false,
1485
+ summary: `Tool "${call.name}" tidak dikenal`,
1486
+ snapshot,
1487
+ block: {
1488
+ type: "tool_result",
1489
+ toolUseId: call.id,
1490
+ isError: true,
1491
+ content: JSON.stringify({
1492
+ ok: false,
1493
+ error: `Unknown tool "${call.name}". Available tools: ${available}.`
1494
+ })
1495
+ }
1496
+ };
1497
+ }
1498
+ let result;
1499
+ try {
1500
+ result = await tool.execute(call.input ?? {}, {
1501
+ document: snapshot,
1502
+ catalog: this.catalog,
1503
+ securityLimits: this.options.securityLimits,
1504
+ signal,
1505
+ generateSection: this.options.engine ? async ({ prompt, parentContext }) => {
1506
+ const response = await this.options.engine.generateSection(
1507
+ { prompt, parentContext },
1508
+ { signal }
1509
+ );
1510
+ if (!response.success || !response.data) {
1511
+ throw new Error(response.error?.message ?? "Section generation failed");
1512
+ }
1513
+ return response.data;
1514
+ } : void 0
1515
+ });
1516
+ } catch (err) {
1517
+ const message = err instanceof Error ? err.message : String(err);
1518
+ this.log("error", `Tool "${call.name}" threw`, message);
1519
+ return {
1520
+ ok: false,
1521
+ summary: `Tool "${call.name}" error`,
1522
+ snapshot,
1523
+ block: {
1524
+ type: "tool_result",
1525
+ toolUseId: call.id,
1526
+ isError: true,
1527
+ content: JSON.stringify({ ok: false, error: message })
1528
+ }
1529
+ };
1530
+ }
1531
+ const op = result.ok && result.op ? {
1532
+ id: call.id,
1533
+ op: result.op,
1534
+ summary: result.summary,
1535
+ destructive: tool.destructive === true
1536
+ } : void 0;
1537
+ return {
1538
+ ok: result.ok,
1539
+ summary: result.summary,
1540
+ snapshot: result.document ?? snapshot,
1541
+ op,
1542
+ block: {
1543
+ type: "tool_result",
1544
+ toolUseId: call.id,
1545
+ isError: !result.ok,
1546
+ content: JSON.stringify(result.content)
1547
+ }
1548
+ };
1549
+ }
1550
+ /**
1551
+ * Executes one agent turn, streaming progress as `AiStreamEvent`s and ending with a
1552
+ * terminal `agent-complete` (or `error`) event.
1553
+ *
1554
+ * The loop stops as soon as the model returns a turn with no tool calls — that final
1555
+ * message is the summary shown to the user. It also stops at `maxSteps` (cost guard) or
1556
+ * on abort, and reports which of those happened via `stoppedBy` so the UI can say
1557
+ * "stopped after N steps" rather than pretending the work is finished.
1558
+ */
1559
+ async *run(request, context) {
1560
+ const maxSteps = request.maxSteps ?? this.options.maxAgentSteps ?? DEFAULT_MAX_STEPS;
1561
+ const signal = context?.signal;
1562
+ if (!request.messages || request.messages.length === 0) {
1563
+ yield {
1564
+ type: "error",
1565
+ error: { code: "INVALID_AGENT_REQUEST", message: '"messages" must not be empty' }
1566
+ };
1567
+ return;
1568
+ }
1569
+ if (!request.document?.document) {
1570
+ yield {
1571
+ type: "error",
1572
+ error: { code: "INVALID_AGENT_REQUEST", message: '"document" is required for agent mode' }
1573
+ };
1574
+ return;
1575
+ }
1576
+ const systemPrompt = buildAgentSystemPrompt({
1577
+ catalog: this.catalog,
1578
+ document: request.document,
1579
+ selectedNodeId: request.selectedNodeId,
1580
+ prefix: this.options.systemPromptPrefix,
1581
+ stylePreference: request.stylePreference,
1582
+ additionalContext: request.systemPrompt
1583
+ });
1584
+ const toolDefinitions = toToolDefinitions(this.tools);
1585
+ const messages = [...request.messages];
1586
+ const ops = [];
1587
+ let snapshot = request.document;
1588
+ let promptTokens = 0;
1589
+ let completionTokens = 0;
1590
+ let summary = "";
1591
+ let stoppedBy = "complete";
1592
+ let step = 0;
1593
+ this.log("info", `[AGENT] run started (maxSteps=${maxSteps}, tools=${toolDefinitions.length})`);
1594
+ try {
1595
+ while (step < maxSteps) {
1596
+ if (signal?.aborted) {
1597
+ stoppedBy = "aborted";
1598
+ break;
1599
+ }
1600
+ step++;
1601
+ yield { type: "agent-step", step, maxSteps };
1602
+ const result = await this.options.adapter.generate({
1603
+ systemPrompt,
1604
+ userPrompt: getMessageText(request.messages[request.messages.length - 1]),
1605
+ messages,
1606
+ tools: toolDefinitions,
1607
+ toolChoice: "auto",
1608
+ signal
1609
+ });
1610
+ promptTokens += result.usage?.promptTokens ?? 0;
1611
+ completionTokens += result.usage?.completionTokens ?? 0;
1612
+ const toolCalls = result.toolCalls ?? [];
1613
+ if (toolCalls.length === 0) {
1614
+ summary = result.text.trim();
1615
+ stoppedBy = "complete";
1616
+ this.log("info", `[AGENT] finished after ${step} step(s) with ${ops.length} op(s)`);
1617
+ break;
1618
+ }
1619
+ const assistantBlocks = [];
1620
+ if (result.text.trim()) {
1621
+ assistantBlocks.push({ type: "text", text: result.text });
1622
+ }
1623
+ assistantBlocks.push(...toolCalls);
1624
+ messages.push({ role: "assistant", content: assistantBlocks, timestamp: Date.now() });
1625
+ const capped = toolCalls.slice(0, MAX_TOOL_CALLS_PER_STEP);
1626
+ const resultBlocks = [];
1627
+ for (const call of capped) {
1628
+ if (signal?.aborted) {
1629
+ stoppedBy = "aborted";
1630
+ break;
1631
+ }
1632
+ yield { type: "tool-call", id: call.id, name: call.name, input: call.input ?? {} };
1633
+ const executed = await this.executeToolCall(call, snapshot, signal);
1634
+ snapshot = executed.snapshot;
1635
+ resultBlocks.push(executed.block);
1636
+ if (executed.op) ops.push(executed.op);
1637
+ yield {
1638
+ type: "tool-result",
1639
+ id: call.id,
1640
+ name: call.name,
1641
+ ok: executed.ok,
1642
+ summary: executed.summary
1643
+ };
1644
+ }
1645
+ for (const call of toolCalls.slice(capped.length)) {
1646
+ resultBlocks.push({
1647
+ type: "tool_result",
1648
+ toolUseId: call.id,
1649
+ isError: true,
1650
+ content: JSON.stringify({
1651
+ ok: false,
1652
+ error: `Too many tool calls in one step (limit ${MAX_TOOL_CALLS_PER_STEP}). This call was not executed \u2014 request it again in the next step.`
1653
+ })
1654
+ });
1655
+ }
1656
+ messages.push(toolResultMessage(resultBlocks));
1657
+ if (stoppedBy === "aborted") break;
1658
+ }
1659
+ if (stoppedBy === "complete" && step >= maxSteps && !summary) {
1660
+ stoppedBy = "max-steps";
1661
+ summary = `Berhenti setelah ${maxSteps} langkah. ${ops.length} perubahan sudah disiapkan \u2014 periksa hasilnya lalu minta lanjutan bila perlu.`;
1662
+ this.log("warn", `[AGENT] hit maxSteps (${maxSteps}) with ${ops.length} op(s)`);
1663
+ }
1664
+ if (stoppedBy === "aborted" && !summary) {
1665
+ summary = `Dihentikan. ${ops.length} perubahan sempat disiapkan.`;
1666
+ }
1667
+ yield {
1668
+ type: "agent-complete",
1669
+ result: {
1670
+ ops,
1671
+ summary,
1672
+ stepsUsed: step,
1673
+ stoppedBy,
1674
+ usage: { promptTokens, completionTokens }
1675
+ }
1676
+ };
1677
+ } catch (err) {
1678
+ const message = err instanceof Error ? err.message : String(err);
1679
+ this.log("error", `[AGENT] run failed: ${message}`);
1680
+ yield {
1681
+ type: "agent-complete",
1682
+ result: {
1683
+ ops,
1684
+ summary: summary || `Terjadi error: ${message}`,
1685
+ stepsUsed: step,
1686
+ stoppedBy: "error",
1687
+ usage: { promptTokens, completionTokens }
1688
+ }
1689
+ };
1690
+ yield { type: "error", error: { code: "AGENT_ERROR", message } };
1691
+ }
1692
+ }
1693
+ /** Non-streaming convenience wrapper — drains `run()` and returns its terminal result. */
1694
+ async execute(request, context) {
1695
+ let final = null;
1696
+ let error = null;
1697
+ for await (const event of this.run(request, context)) {
1698
+ if (event.type === "agent-complete") final = event.result;
1699
+ if (event.type === "error") error = event.error;
1700
+ }
1701
+ if (final) return final;
1702
+ return {
1703
+ ops: [],
1704
+ summary: error?.message ?? "Agent produced no result",
1705
+ stepsUsed: 0,
1706
+ stoppedBy: "error"
1707
+ };
1708
+ }
1709
+ };
1710
+
567
1711
  // src/server/handler.ts
568
- async function processAiRequest(engine, body, signal) {
1712
+ async function processAiRequest(engine, body, signal, agent) {
569
1713
  if (!body || typeof body !== "object") {
570
1714
  return {
571
1715
  status: 400,
@@ -599,7 +1743,10 @@ async function processAiRequest(engine, body, signal) {
599
1743
  stylePreference: payload.stylePreference,
600
1744
  tone: payload.tone,
601
1745
  locale: payload.locale,
602
- metadata: payload.metadata
1746
+ metadata: payload.metadata,
1747
+ conversationHistory: payload.conversationHistory ?? payload.messages,
1748
+ sectionCount: payload.sectionCount,
1749
+ plan: payload.plan
603
1750
  },
604
1751
  { signal }
605
1752
  );
@@ -687,17 +1834,142 @@ async function processAiRequest(engine, body, signal) {
687
1834
  response: result
688
1835
  };
689
1836
  }
1837
+ if (mode === "agent") {
1838
+ const validationError = validateAgentPayload(payload, agent);
1839
+ if (validationError) {
1840
+ return { status: validationError.status, response: validationError.response };
1841
+ }
1842
+ const result = await agent.execute(
1843
+ {
1844
+ messages: payload.messages,
1845
+ document: payload.document,
1846
+ selectedNodeId: payload.selectedNodeId,
1847
+ stylePreference: payload.stylePreference,
1848
+ maxSteps: payload.maxSteps
1849
+ },
1850
+ { signal }
1851
+ );
1852
+ return {
1853
+ status: result.stoppedBy === "error" ? 500 : 200,
1854
+ response: { success: result.stoppedBy !== "error", data: result }
1855
+ };
1856
+ }
1857
+ if (mode === "plan") {
1858
+ if (!payload.prompt || typeof payload.prompt !== "string") {
1859
+ return {
1860
+ status: 400,
1861
+ response: {
1862
+ success: false,
1863
+ error: {
1864
+ code: "INVALID_PROMPT",
1865
+ message: '"prompt" is required for planning mode'
1866
+ }
1867
+ }
1868
+ };
1869
+ }
1870
+ const result = await engine.planPage(
1871
+ {
1872
+ prompt: payload.prompt,
1873
+ stylePreference: payload.stylePreference,
1874
+ tone: payload.tone,
1875
+ locale: payload.locale,
1876
+ sectionCount: payload.sectionCount,
1877
+ conversationHistory: payload.conversationHistory ?? payload.messages
1878
+ },
1879
+ { signal }
1880
+ );
1881
+ return {
1882
+ status: result.success ? 200 : 500,
1883
+ response: result
1884
+ };
1885
+ }
690
1886
  return {
691
1887
  status: 400,
692
1888
  response: {
693
1889
  success: false,
694
1890
  error: {
695
1891
  code: "UNKNOWN_MODE",
696
- message: `Unsupported mode: ${String(mode)}. Supported modes: 'full-page', 'section', 'refactor', 'chat'`
1892
+ message: `Unsupported mode: ${String(mode)}. Supported modes: 'full-page', 'section', 'refactor', 'chat', 'agent', 'plan'`
697
1893
  }
698
1894
  }
699
1895
  };
700
1896
  }
1897
+ function validateAgentPayload(payload, agent) {
1898
+ if (!agent) {
1899
+ return {
1900
+ status: 501,
1901
+ response: {
1902
+ success: false,
1903
+ error: {
1904
+ code: "AGENT_NOT_CONFIGURED",
1905
+ message: "Agent mode is not enabled on this endpoint. Pass a KubuildAiAgent to createAiHandler to enable it."
1906
+ }
1907
+ }
1908
+ };
1909
+ }
1910
+ if (!payload.messages || !Array.isArray(payload.messages) || payload.messages.length === 0) {
1911
+ return {
1912
+ status: 400,
1913
+ response: {
1914
+ success: false,
1915
+ error: {
1916
+ code: "INVALID_AGENT_PARAMS",
1917
+ message: '"messages" array is required and must not be empty for agent mode'
1918
+ }
1919
+ }
1920
+ };
1921
+ }
1922
+ if (!payload.document || !payload.document.document) {
1923
+ return {
1924
+ status: 400,
1925
+ response: {
1926
+ success: false,
1927
+ error: {
1928
+ code: "INVALID_AGENT_PARAMS",
1929
+ message: '"document" (the current PageDocument) is required for agent mode'
1930
+ }
1931
+ }
1932
+ };
1933
+ }
1934
+ return null;
1935
+ }
1936
+ function sseResponse(events, errorCode) {
1937
+ const encoder = new TextEncoder();
1938
+ const readable = new ReadableStream({
1939
+ async start(controller) {
1940
+ try {
1941
+ for await (const event of events) {
1942
+ controller.enqueue(
1943
+ encoder.encode(`event: ${event.type}
1944
+ data: ${JSON.stringify(event)}
1945
+
1946
+ `)
1947
+ );
1948
+ }
1949
+ } catch (err) {
1950
+ const message = err instanceof Error ? err.message : String(err);
1951
+ controller.enqueue(
1952
+ encoder.encode(
1953
+ `event: error
1954
+ data: ${JSON.stringify({ type: "error", error: { code: errorCode, message } })}
1955
+
1956
+ `
1957
+ )
1958
+ );
1959
+ } finally {
1960
+ controller.close();
1961
+ }
1962
+ }
1963
+ });
1964
+ return new Response(readable, {
1965
+ status: 200,
1966
+ headers: {
1967
+ "Content-Type": "text/event-stream",
1968
+ "Cache-Control": "no-cache, no-transform",
1969
+ Connection: "keep-alive"
1970
+ }
1971
+ });
1972
+ }
701
1973
  function createAiHandler(engine, options) {
702
1974
  return async (request) => {
703
1975
  try {
@@ -728,6 +2000,28 @@ function createAiHandler(engine, options) {
728
2000
  const body = await request.json().catch(() => null);
729
2001
  if (body && typeof body === "object" && body.stream === true) {
730
2002
  const mode = body.mode || "full-page";
2003
+ if (mode === "agent") {
2004
+ const validationError = validateAgentPayload(body, options?.agent);
2005
+ if (validationError) {
2006
+ return new Response(JSON.stringify(validationError.response), {
2007
+ status: validationError.status,
2008
+ headers: { "Content-Type": "application/json" }
2009
+ });
2010
+ }
2011
+ return sseResponse(
2012
+ options.agent.run(
2013
+ {
2014
+ messages: body.messages,
2015
+ document: body.document,
2016
+ selectedNodeId: body.selectedNodeId,
2017
+ stylePreference: body.stylePreference,
2018
+ maxSteps: body.maxSteps
2019
+ },
2020
+ { signal: request.signal }
2021
+ ),
2022
+ "AGENT_STREAM_ERROR"
2023
+ );
2024
+ }
731
2025
  if (mode === "chat") {
732
2026
  if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
733
2027
  return new Response(
@@ -744,48 +2038,17 @@ function createAiHandler(engine, options) {
744
2038
  }
745
2039
  );
746
2040
  }
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
- });
2041
+ return sseResponse(
2042
+ engine.chatStream(
2043
+ {
2044
+ messages: body.messages,
2045
+ currentDocument: body.currentDocument,
2046
+ selectedNodeId: body.selectedNodeId
2047
+ },
2048
+ { signal: request.signal }
2049
+ ),
2050
+ "CHAT_STREAM_ERROR"
2051
+ );
789
2052
  }
790
2053
  if (!body.prompt || typeof body.prompt !== "string") {
791
2054
  return new Response(
@@ -802,55 +2065,28 @@ data: ${JSON.stringify({ type: "error", error: { code: "CHAT_STREAM_ERROR", mess
802
2065
  }
803
2066
  );
804
2067
  }
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
- });
2068
+ return sseResponse(
2069
+ engine.streamPage(
2070
+ {
2071
+ prompt: body.prompt,
2072
+ stylePreference: body.stylePreference,
2073
+ tone: body.tone,
2074
+ locale: body.locale,
2075
+ metadata: body.metadata,
2076
+ conversationHistory: body.conversationHistory ?? body.messages,
2077
+ sectionCount: body.sectionCount,
2078
+ plan: body.plan
2079
+ },
2080
+ { signal: request.signal }
2081
+ ),
2082
+ "STREAM_ERROR"
2083
+ );
849
2084
  }
850
2085
  const { status, response } = await processAiRequest(
851
2086
  engine,
852
2087
  body,
853
- request.signal
2088
+ request.signal,
2089
+ options?.agent
854
2090
  );
855
2091
  return new Response(JSON.stringify(response), {
856
2092
  status,
@@ -882,9 +2118,19 @@ export {
882
2118
  AnthropicAdapter,
883
2119
  CustomHttpAdapter,
884
2120
  GeminiAdapter,
2121
+ KubuildAiAgent,
885
2122
  KubuildAiEngine,
886
2123
  OpenAiAdapter,
2124
+ READ_TOOLS,
2125
+ WRITE_TOOLS,
2126
+ checkComponentType,
2127
+ checkDocumentSecurity,
2128
+ checkNesting,
887
2129
  createAiHandler,
888
- processAiRequest
2130
+ createDocumentTools,
2131
+ normalizeIncomingNode,
2132
+ processAiRequest,
2133
+ suggestNodeIds,
2134
+ toToolDefinitions
889
2135
  };
890
2136
  //# sourceMappingURL=index.js.map