@kubuild/ai 0.6.0 → 0.7.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.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ appendClientInstructions,
2
3
  buildAgentSystemPrompt,
3
4
  buildJsonSchemaForMode,
4
5
  buildSelectionContext,
@@ -6,13 +7,14 @@ import {
6
7
  compileComponentCatalog,
7
8
  extractJsonFromResponse,
8
9
  getNodeLabel,
10
+ joinInstructions,
9
11
  normalizeAndValidatePageDocument,
10
12
  normalizeAndValidateRefactoredNode,
11
13
  normalizeAndValidateSectionNode,
12
14
  normalizeNodeTree,
13
15
  pruneNodeForPrompt,
14
16
  summarizeNodeTree
15
- } from "../chunk-YETTTWHI.js";
17
+ } from "../chunk-CCV6S4YN.js";
16
18
  import {
17
19
  AnthropicAdapter,
18
20
  CustomHttpAdapter,
@@ -24,7 +26,40 @@ import {
24
26
  } from "../chunk-ILLNRABB.js";
25
27
 
26
28
  // src/server/engine.ts
27
- import { DocumentMetadataSchema } from "@kubuild/schema";
29
+ import { DocumentMetadataSchema, CURRENT_SCHEMA_VERSION } from "@kubuild/schema";
30
+ function describeSectionCount(sectionCount) {
31
+ if (typeof sectionCount === "number" && Number.isFinite(sectionCount) && sectionCount > 0) {
32
+ return `exactly ${Math.floor(sectionCount)} cohesive sections`;
33
+ }
34
+ if (sectionCount && typeof sectionCount === "object" && (sectionCount.min || sectionCount.max)) {
35
+ const min = sectionCount.min ?? 3;
36
+ const max = sectionCount.max ?? 8;
37
+ return `between ${min} and ${max} cohesive sections`;
38
+ }
39
+ return null;
40
+ }
41
+ function hasPlannedSections(plan) {
42
+ return !!plan && Array.isArray(plan.sections) && plan.sections.length > 0;
43
+ }
44
+ function buildFallbackPlan(prompt) {
45
+ return {
46
+ title: "AI Generated Page",
47
+ description: "Generated by KUBUILD AI",
48
+ sections: [
49
+ { type: "hero", title: "Hero Banner", prompt: `Hero section for: ${prompt}` },
50
+ { type: "features", title: "Key Features", prompt: `Features grid for: ${prompt}` },
51
+ {
52
+ type: "testimonials",
53
+ title: "Testimonials",
54
+ prompt: `Social proof and customer reviews for: ${prompt}`
55
+ },
56
+ { type: "cta", title: "Call To Action", prompt: `Call to action section for: ${prompt}` },
57
+ { type: "footer", title: "Footer", prompt: `Footer navigation and copyright for: ${prompt}` }
58
+ ],
59
+ usedFallback: true
60
+ };
61
+ }
62
+ var PLAN_FALLBACK_MESSAGE = "The AI response could not be parsed as a page plan, so a generic default plan was used instead.";
28
63
  var KubuildAiEngine = class {
29
64
  options;
30
65
  catalog;
@@ -50,20 +85,31 @@ var KubuildAiEngine = class {
50
85
  }
51
86
  }
52
87
  async generatePage(request, context) {
88
+ if (hasPlannedSections(request.plan)) {
89
+ return this.generatePageFromPlan(request, context);
90
+ }
53
91
  let rawText = "";
54
92
  try {
55
93
  this.log("info", `Generating full page for prompt: "${request.prompt}"`);
56
- const systemPrompt = buildSystemPrompt({
57
- catalog: this.catalog,
58
- mode: "full-page",
59
- prefix: this.options.systemPromptPrefix,
60
- stylePreference: request.stylePreference
61
- });
94
+ const systemPrompt = appendClientInstructions(
95
+ buildSystemPrompt({
96
+ catalog: this.catalog,
97
+ mode: "full-page",
98
+ prefix: this.options.systemPromptPrefix,
99
+ stylePreference: request.stylePreference
100
+ }),
101
+ request.instructions
102
+ );
62
103
  let userPrompt = `User Request: ${request.prompt}`;
63
104
  if (request.tone) userPrompt += `
64
105
  Tone: ${request.tone}`;
65
106
  if (request.locale) userPrompt += `
66
107
  Language/Locale: ${request.locale}`;
108
+ const sectionCountPhrase = describeSectionCount(request.sectionCount);
109
+ if (sectionCountPhrase) {
110
+ userPrompt += `
111
+ Section Count: the page root must contain ${sectionCountPhrase} as its direct children.`;
112
+ }
67
113
  if (request.conversationHistory && request.conversationHistory.length > 0) {
68
114
  const chatContext = request.conversationHistory.map((m) => `${m.role.toUpperCase()}: ${getMessageText(m)}`).join("\n");
69
115
  userPrompt += `
@@ -114,15 +160,42 @@ ${chatContext}`;
114
160
  };
115
161
  }
116
162
  }
163
+ /**
164
+ * Non-streaming counterpart of `streamPage` for a pre-approved plan: drains the same
165
+ * progressive pipeline and returns the assembled document, so both paths produce the
166
+ * same sections for the same plan.
167
+ */
168
+ async generatePageFromPlan(request, context) {
169
+ let document = null;
170
+ let error = null;
171
+ for await (const event of this.streamPage(request, context)) {
172
+ if (event.type === "complete") document = event.document;
173
+ if (event.type === "error") error = event.error;
174
+ }
175
+ if (document) {
176
+ return { success: true, data: document };
177
+ }
178
+ return {
179
+ success: false,
180
+ error: {
181
+ code: "GENERATION_ERROR",
182
+ message: error?.message ?? "Page generation from the approved plan produced no document.",
183
+ details: error ? { streamCode: error.code } : void 0
184
+ }
185
+ };
186
+ }
117
187
  async generateSection(request, context) {
118
188
  let rawText = "";
119
189
  try {
120
- const systemPrompt = buildSystemPrompt({
121
- catalog: this.catalog,
122
- mode: "section",
123
- prefix: this.options.systemPromptPrefix,
124
- stylePreference: request.stylePreference
125
- });
190
+ const systemPrompt = appendClientInstructions(
191
+ buildSystemPrompt({
192
+ catalog: this.catalog,
193
+ mode: "section",
194
+ prefix: this.options.systemPromptPrefix,
195
+ stylePreference: request.stylePreference
196
+ }),
197
+ request.instructions
198
+ );
126
199
  let userPrompt = `Generate a single section node for: ${request.prompt}`;
127
200
  if (request.targetSectionType) {
128
201
  userPrompt += `
@@ -167,12 +240,15 @@ Surrounding Page Context: ${request.parentContext}`;
167
240
  async refactorNode(request, context) {
168
241
  let rawText = "";
169
242
  try {
170
- const systemPrompt = buildSystemPrompt({
171
- catalog: this.catalog,
172
- mode: "refactor",
173
- prefix: this.options.systemPromptPrefix,
174
- stylePreference: request.stylePreference
175
- });
243
+ const systemPrompt = appendClientInstructions(
244
+ buildSystemPrompt({
245
+ catalog: this.catalog,
246
+ mode: "refactor",
247
+ prefix: this.options.systemPromptPrefix,
248
+ stylePreference: request.stylePreference
249
+ }),
250
+ request.instructions
251
+ );
176
252
  const userPrompt = `Instruction: ${request.instruction}
177
253
 
178
254
  Current Node:
@@ -210,16 +286,8 @@ ${JSON.stringify(request.node, null, 2)}`;
210
286
  }
211
287
  }
212
288
  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
- }
289
+ const sectionCountPhrase = describeSectionCount(request.sectionCount);
290
+ const sectionGuidance = sectionCountPhrase ? `Plan ${sectionCountPhrase} that fulfill the request.` : "Plan between 4 to 6 cohesive, essential sections (e.g., hero, features, testimonials, pricing, cta, footer) that fulfill the request thoroughly. If the user request mentions a specific number or list of sections, respect the user request.";
223
291
  const planSystemPrompt = `
224
292
  You are a web architect for the KUBUILD page builder.
225
293
  Given the user's prompt, plan the website structure. Output pure JSON (no markdown fences, no explanatory text):
@@ -260,46 +328,59 @@ Locale: ${request.locale}`;
260
328
  Prior Conversation Discussion Context:
261
329
  ${chatContext}`;
262
330
  }
263
- return { systemPrompt: planSystemPrompt, userPrompt: planUserPrompt };
331
+ return {
332
+ systemPrompt: appendClientInstructions(planSystemPrompt, request.instructions),
333
+ userPrompt: planUserPrompt
334
+ };
264
335
  }
265
336
  async planPage(request, context) {
266
- let rawText = "";
337
+ const { systemPrompt, userPrompt } = this.buildPlanPrompts(request);
338
+ this.log("info", `Planning website layout for: "${request.prompt}"`);
339
+ let planResult;
267
340
  try {
268
- const { systemPrompt, userPrompt } = this.buildPlanPrompts(request);
269
- this.log("info", `Planning website layout for: "${request.prompt}"`);
270
- const planResult = await this.options.adapter.generate({
341
+ planResult = await this.options.adapter.generate({
271
342
  systemPrompt,
272
343
  userPrompt,
273
344
  signal: context?.signal
274
345
  });
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
346
  } catch (err) {
284
347
  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
- ]
348
+ this.log("error", `Planning failed: ${message}`);
349
+ return {
350
+ success: false,
351
+ error: { code: "PLAN_ERROR", message }
296
352
  };
353
+ }
354
+ const rawText = planResult.text;
355
+ let plan = null;
356
+ let parseError = "";
357
+ try {
358
+ const parsed = extractJsonFromResponse(rawText);
359
+ if (hasPlannedSections(parsed)) {
360
+ plan = parsed;
361
+ } else {
362
+ parseError = 'Plan JSON has no non-empty "sections" array';
363
+ }
364
+ } catch (err) {
365
+ parseError = err instanceof Error ? err.message : String(err);
366
+ }
367
+ if (plan) {
297
368
  return {
298
369
  success: true,
299
- data: fallbackPlan,
300
- rawModelResponse: rawText || void 0
370
+ data: plan,
371
+ usage: planResult.usage,
372
+ rawModelResponse: rawText
301
373
  };
302
374
  }
375
+ this.log("warn", "Failed to parse plan JSON, using fallback plan", parseError);
376
+ return {
377
+ success: true,
378
+ data: buildFallbackPlan(request.prompt),
379
+ usedFallback: true,
380
+ warnings: [{ code: "PLAN_FALLBACK", message: PLAN_FALLBACK_MESSAGE, details: parseError }],
381
+ usage: planResult.usage,
382
+ rawModelResponse: rawText || void 0
383
+ };
303
384
  }
304
385
  /**
305
386
  * Progressive Section Streaming Generator.
@@ -313,7 +394,7 @@ ${chatContext}`;
313
394
  message: "Analyzing requirements and planning page sections..."
314
395
  };
315
396
  let plan;
316
- if (request.plan && Array.isArray(request.plan.sections) && request.plan.sections.length > 0) {
397
+ if (hasPlannedSections(request.plan)) {
317
398
  plan = request.plan;
318
399
  this.log("info", `[SSE] Using approved pre-planned structure with ${plan.sections?.length ?? 0} sections`);
319
400
  } else {
@@ -325,31 +406,21 @@ ${chatContext}`;
325
406
  signal: context?.signal
326
407
  });
327
408
  this.log("debug", "[SSE] Raw plan response from model", planResult.text);
409
+ let parsedPlan = null;
328
410
  try {
329
- plan = extractJsonFromResponse(planResult.text);
330
- this.log("debug", "[SSE] Parsed plan successfully", plan);
411
+ parsedPlan = extractJsonFromResponse(planResult.text);
412
+ this.log("debug", "[SSE] Parsed plan successfully", parsedPlan);
331
413
  } catch (parseErr) {
332
414
  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
- };
415
+ }
416
+ if (hasPlannedSections(parsedPlan)) {
417
+ plan = parsedPlan;
418
+ } else {
419
+ plan = buildFallbackPlan(request.prompt);
420
+ yield { type: "status", message: PLAN_FALLBACK_MESSAGE };
344
421
  }
345
422
  }
346
- const sectionsToGenerate = Array.isArray(plan.sections) && plan.sections.length > 0 ? plan.sections : [
347
- { type: "hero", title: "Hero Section", prompt: `Hero banner for: ${request.prompt}` },
348
- { type: "features", title: "Features", prompt: `Features grid 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}` }
352
- ];
423
+ const sectionsToGenerate = hasPlannedSections(plan) ? plan.sections : buildFallbackPlan(request.prompt).sections;
353
424
  const metadata = DocumentMetadataSchema.parse({
354
425
  title: plan.title || "AI Generated Page",
355
426
  description: plan.description || "Generated by KUBUILD AI",
@@ -406,6 +477,7 @@ ${chatContext}`;
406
477
  {
407
478
  prompt: plannedSec.prompt,
408
479
  stylePreference: request.stylePreference,
480
+ instructions: request.instructions,
409
481
  targetSectionType: plannedSec.type,
410
482
  parentContext: `Website: ${plan.title}. Previous sections: ${sectionsToGenerate.slice(0, i).map((s) => s.title).join(", ")}`
411
483
  },
@@ -440,7 +512,7 @@ ${chatContext}`;
440
512
  }
441
513
  const finalDocument = {
442
514
  schema: "stora.page",
443
- version: "1.0.0",
515
+ version: CURRENT_SCHEMA_VERSION,
444
516
  metadata,
445
517
  document: {
446
518
  ...rootPageNode,
@@ -484,11 +556,6 @@ ${this.catalog.map((c) => `- **${c.type}** (${c.category}): ${c.label}${c.descri
484
556
  systemPrompt = `${this.options.systemPromptPrefix}
485
557
 
486
558
  ${systemPrompt}`;
487
- }
488
- if (request.systemPrompt) {
489
- systemPrompt += `
490
- Additional Context:
491
- ${request.systemPrompt}`;
492
559
  }
493
560
  if (request.currentDocument) {
494
561
  const doc = request.currentDocument;
@@ -507,6 +574,10 @@ ${sectionSummary || "(Canvas is currently empty)"}
507
574
  systemPrompt += `
508
575
  Currently Selected Component Node ID: "${request.selectedNodeId}"`;
509
576
  }
577
+ systemPrompt = appendClientInstructions(
578
+ systemPrompt,
579
+ joinInstructions(request.instructions, request.systemPrompt)
580
+ );
510
581
  const lastUserMsg = [...request.messages].reverse().find((m) => m.role === "user");
511
582
  const userPrompt = lastUserMsg ? getMessageText(lastUserMsg) || "Hello" : "Hello";
512
583
  return { systemPrompt, userPrompt };
@@ -695,7 +766,7 @@ function resolveNode(document, nodeId, toolName) {
695
766
  if (!node) {
696
767
  return {
697
768
  error: fail(
698
- `${toolName}: node "${nodeId}" tidak ditemukan`,
769
+ `${toolName}: node "${nodeId}" not found`,
699
770
  `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
771
  { suggestedNodeIds: suggestNodeIds(document, nodeId) }
701
772
  )
@@ -789,7 +860,7 @@ var getPageOutline = {
789
860
  },
790
861
  execute(input, context) {
791
862
  const maxDepth = typeof input.maxDepth === "number" && input.maxDepth > 0 ? Math.floor(input.maxDepth) : 3;
792
- return succeed("Membaca struktur halaman", {
863
+ return succeed("Read page outline", {
793
864
  outline: summarizeNodeTree(context.document.document, { maxDepth })
794
865
  });
795
866
  }
@@ -813,7 +884,7 @@ var readNode = {
813
884
  },
814
885
  execute(input, context) {
815
886
  const nodeId = readString(input, "nodeId");
816
- if (!nodeId) return fail("read_node: nodeId kosong", 'Argument "nodeId" is required.');
887
+ if (!nodeId) return fail("read_node: nodeId missing", 'Argument "nodeId" is required.');
817
888
  const resolved = resolveNode(context.document, nodeId, "read_node");
818
889
  if ("error" in resolved) return resolved.error;
819
890
  const selection = buildSelectionContext(context.document.document, nodeId);
@@ -859,7 +930,7 @@ var findNodes = {
859
930
  const limit = typeof input.limit === "number" && input.limit > 0 ? Math.floor(input.limit) : 20;
860
931
  if (!type && !textContains) {
861
932
  return fail(
862
- "find_nodes: filter kosong",
933
+ "find_nodes: empty filter",
863
934
  'Provide at least one of "type" or "textContains" \u2014 an unfiltered search would just return the whole outline.'
864
935
  );
865
936
  }
@@ -907,7 +978,7 @@ var listComponentTypes = {
907
978
  execute(input, context) {
908
979
  const category = readString(input, "category");
909
980
  const entries = category ? context.catalog.filter((spec) => spec.category === category) : context.catalog;
910
- return succeed(`Melihat katalog komponen (${entries.length})`, {
981
+ return succeed(`Viewed component catalog (${entries.length})`, {
911
982
  components: entries.map((spec) => ({
912
983
  type: spec.type,
913
984
  label: spec.label,
@@ -942,14 +1013,14 @@ function applyCommand(toolName, run) {
942
1013
  return run();
943
1014
  } catch (err) {
944
1015
  const message = err instanceof Error ? err.message : String(err);
945
- return { error: fail(`${toolName} gagal`, message) };
1016
+ return { error: fail(`${toolName} failed`, message) };
946
1017
  }
947
1018
  }
948
1019
  function guardSecurity(toolName, document, context) {
949
1020
  const violation = checkDocumentSecurity(document, context.securityLimits);
950
1021
  if (!violation) return null;
951
1022
  return fail(
952
- `${toolName} ditolak (security)`,
1023
+ `${toolName} rejected (security)`,
953
1024
  `The resulting document failed the security check and was rejected: ${violation}`
954
1025
  );
955
1026
  }
@@ -976,11 +1047,11 @@ var updateNodeProps = {
976
1047
  },
977
1048
  execute(input, context) {
978
1049
  const nodeId = readString(input, "nodeId");
979
- if (!nodeId) return fail("update_node_props: nodeId kosong", 'Argument "nodeId" is required.');
1050
+ if (!nodeId) return fail("update_node_props: nodeId missing", 'Argument "nodeId" is required.');
980
1051
  const props = readPlainObject(input, "props");
981
1052
  if (!props) {
982
1053
  return fail(
983
- "update_node_props: props tidak valid",
1054
+ "update_node_props: invalid props",
984
1055
  'Argument "props" must be a JSON object of prop names to values.'
985
1056
  );
986
1057
  }
@@ -997,7 +1068,7 @@ var updateNodeProps = {
997
1068
  const op = { kind: "update-props", nodeId, props, merge };
998
1069
  const updated = findNodeById2(applied.document.document, nodeId);
999
1070
  return succeed(
1000
- `Ubah props ${describeNode(resolved.node)}`,
1071
+ `Update props of ${describeNode(resolved.node)}`,
1001
1072
  { nodeId, props: updated?.props ?? props, changedKeys: Object.keys(props) },
1002
1073
  { op, document: applied.document }
1003
1074
  );
@@ -1035,11 +1106,11 @@ var updateNodeStyles = {
1035
1106
  },
1036
1107
  execute(input, context) {
1037
1108
  const nodeId = readString(input, "nodeId");
1038
- if (!nodeId) return fail("update_node_styles: nodeId kosong", 'Argument "nodeId" is required.');
1109
+ if (!nodeId) return fail("update_node_styles: nodeId missing", 'Argument "nodeId" is required.');
1039
1110
  const styles = readPlainObject(input, "styles");
1040
1111
  if (!styles) {
1041
1112
  return fail(
1042
- "update_node_styles: styles tidak valid",
1113
+ "update_node_styles: invalid styles",
1043
1114
  'Argument "styles" must be a JSON object of CSS properties with primitive values.'
1044
1115
  );
1045
1116
  }
@@ -1048,7 +1119,7 @@ var updateNodeStyles = {
1048
1119
  );
1049
1120
  if (nested) {
1050
1121
  return fail(
1051
- "update_node_styles: nilai bersarang",
1122
+ "update_node_styles: nested value",
1052
1123
  `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
1124
  );
1054
1125
  }
@@ -1056,13 +1127,13 @@ var updateNodeStyles = {
1056
1127
  const rawBreakpoint = readString(input, "breakpoint");
1057
1128
  if (rawState && rawBreakpoint) {
1058
1129
  return fail(
1059
- "update_node_styles: argumen bentrok",
1130
+ "update_node_styles: conflicting arguments",
1060
1131
  'Pass either "breakpoint" or "state", not both \u2014 a pseudo-state layer is not per-breakpoint.'
1061
1132
  );
1062
1133
  }
1063
1134
  if (rawBreakpoint && !BREAKPOINTS.includes(rawBreakpoint)) {
1064
1135
  return fail(
1065
- "update_node_styles: breakpoint tidak dikenal",
1136
+ "update_node_styles: unknown breakpoint",
1066
1137
  `Unknown breakpoint "${rawBreakpoint}". Valid values: ${BREAKPOINTS.join(", ")}.`
1067
1138
  );
1068
1139
  }
@@ -1081,7 +1152,7 @@ var updateNodeStyles = {
1081
1152
  const op = { kind: "update-styles", nodeId, styles, breakpoint, state, merge };
1082
1153
  const layer = state ? `state ${state}` : `breakpoint ${breakpoint}`;
1083
1154
  return succeed(
1084
- `Ubah style ${describeNode(resolved.node)} (${layer})`,
1155
+ `Update styles of ${describeNode(resolved.node)} (${layer})`,
1085
1156
  { nodeId, layer, appliedStyles: styles },
1086
1157
  { op, document: applied.document }
1087
1158
  );
@@ -1120,16 +1191,16 @@ var insertComponent = {
1120
1191
  const type = readString(input, "type");
1121
1192
  if (!parentId || !type) {
1122
1193
  return fail(
1123
- "insert_component: argumen kurang",
1194
+ "insert_component: missing arguments",
1124
1195
  'Arguments "parentId" and "type" are both required.'
1125
1196
  );
1126
1197
  }
1127
1198
  const typeError = checkComponentType(context.catalog, type);
1128
- if (typeError) return fail("insert_component: tipe tidak dikenal", typeError);
1199
+ if (typeError) return fail("insert_component: unknown type", typeError);
1129
1200
  const resolvedParent = resolveNode(context.document, parentId, "insert_component");
1130
1201
  if ("error" in resolvedParent) return resolvedParent.error;
1131
1202
  const nestingError = checkNesting(context.catalog, resolvedParent.node, type);
1132
- if (nestingError) return fail("insert_component: nesting tidak valid", nestingError);
1203
+ if (nestingError) return fail("insert_component: invalid nesting", nestingError);
1133
1204
  const normalized = normalizeIncomingNode(
1134
1205
  {
1135
1206
  type,
@@ -1140,7 +1211,7 @@ var insertComponent = {
1140
1211
  context.document
1141
1212
  );
1142
1213
  if ("error" in normalized) {
1143
- return fail("insert_component: node tidak valid", normalized.error);
1214
+ return fail("insert_component: invalid node", normalized.error);
1144
1215
  }
1145
1216
  const index = readOptionalIndex(input, "index");
1146
1217
  const applied = applyCommand(
@@ -1152,7 +1223,7 @@ var insertComponent = {
1152
1223
  if (violation) return violation;
1153
1224
  const op = { kind: "insert-node", parentId, index, node: normalized.node };
1154
1225
  return succeed(
1155
- `Tambah ${describeNode(normalized.node)} ke #${parentId}`,
1226
+ `Add ${describeNode(normalized.node)} to #${parentId}`,
1156
1227
  { nodeId: normalized.node.id, parentId, index: index ?? null },
1157
1228
  { op, document: applied.document }
1158
1229
  );
@@ -1180,10 +1251,10 @@ var insertSection = {
1180
1251
  },
1181
1252
  async execute(input, context) {
1182
1253
  const prompt = readString(input, "prompt");
1183
- if (!prompt) return fail("insert_section: prompt kosong", 'Argument "prompt" is required.');
1254
+ if (!prompt) return fail("insert_section: prompt missing", 'Argument "prompt" is required.');
1184
1255
  if (!context.generateSection) {
1185
1256
  return fail(
1186
- "insert_section: tidak tersedia",
1257
+ "insert_section: not available",
1187
1258
  "Section generation is not available in this deployment. Build the section with insert_component instead."
1188
1259
  );
1189
1260
  }
@@ -1197,13 +1268,13 @@ var insertSection = {
1197
1268
  });
1198
1269
  } catch (err) {
1199
1270
  return fail(
1200
- "insert_section gagal",
1271
+ "insert_section failed",
1201
1272
  `Section generation failed: ${err instanceof Error ? err.message : String(err)}`
1202
1273
  );
1203
1274
  }
1204
1275
  const normalized = normalizeIncomingNode(generated, context.document);
1205
1276
  if ("error" in normalized) {
1206
- return fail("insert_section: section tidak valid", normalized.error);
1277
+ return fail("insert_section: invalid section", normalized.error);
1207
1278
  }
1208
1279
  const index = readOptionalIndex(input, "index");
1209
1280
  const applied = applyCommand(
@@ -1215,7 +1286,7 @@ var insertSection = {
1215
1286
  if (violation) return violation;
1216
1287
  const op = { kind: "insert-node", parentId: rootId, index, node: normalized.node };
1217
1288
  return succeed(
1218
- `Tambah section baru (#${normalized.node.id})`,
1289
+ `Add new section (#${normalized.node.id})`,
1219
1290
  {
1220
1291
  nodeId: normalized.node.id,
1221
1292
  index: index ?? null,
@@ -1245,7 +1316,7 @@ var moveNodeTool = {
1245
1316
  const targetParentId = readString(input, "targetParentId");
1246
1317
  if (!nodeId || !targetParentId) {
1247
1318
  return fail(
1248
- "move_node: argumen kurang",
1319
+ "move_node: missing arguments",
1249
1320
  'Arguments "nodeId" and "targetParentId" are both required.'
1250
1321
  );
1251
1322
  }
@@ -1254,7 +1325,7 @@ var moveNodeTool = {
1254
1325
  const resolvedParent = resolveNode(context.document, targetParentId, "move_node");
1255
1326
  if ("error" in resolvedParent) return resolvedParent.error;
1256
1327
  const nestingError = checkNesting(context.catalog, resolvedParent.node, resolved.node.type);
1257
- if (nestingError) return fail("move_node: nesting tidak valid", nestingError);
1328
+ if (nestingError) return fail("move_node: invalid nesting", nestingError);
1258
1329
  const index = readOptionalIndex(input, "index");
1259
1330
  const applied = applyCommand(
1260
1331
  "move_node",
@@ -1265,7 +1336,7 @@ var moveNodeTool = {
1265
1336
  if (violation) return violation;
1266
1337
  const op = { kind: "move-node", nodeId, targetParentId, index };
1267
1338
  return succeed(
1268
- `Pindah ${describeNode(resolved.node)} ke #${targetParentId}`,
1339
+ `Move ${describeNode(resolved.node)} to #${targetParentId}`,
1269
1340
  { nodeId, targetParentId, index: index ?? null },
1270
1341
  { op, document: applied.document }
1271
1342
  );
@@ -1291,7 +1362,7 @@ var duplicateNodeTool = {
1291
1362
  },
1292
1363
  execute(input, context) {
1293
1364
  const nodeId = readString(input, "nodeId");
1294
- if (!nodeId) return fail("duplicate_node: nodeId kosong", 'Argument "nodeId" is required.');
1365
+ if (!nodeId) return fail("duplicate_node: nodeId missing", 'Argument "nodeId" is required.');
1295
1366
  const resolved = resolveNode(context.document, nodeId, "duplicate_node");
1296
1367
  if ("error" in resolved) return resolved.error;
1297
1368
  const targetParentId = readString(input, "targetParentId") ?? void 0;
@@ -1309,7 +1380,7 @@ var duplicateNodeTool = {
1309
1380
  if (violation) return violation;
1310
1381
  const op = { kind: "duplicate-node", nodeId, targetParentId, index };
1311
1382
  return succeed(
1312
- `Duplikat ${describeNode(resolved.node)}`,
1383
+ `Duplicate ${describeNode(resolved.node)}`,
1313
1384
  { nodeId, targetParentId: targetParentId ?? null, index: index ?? null },
1314
1385
  { op, document: applied.document }
1315
1386
  );
@@ -1335,11 +1406,11 @@ var deleteNode = {
1335
1406
  },
1336
1407
  execute(input, context) {
1337
1408
  const nodeId = readString(input, "nodeId");
1338
- if (!nodeId) return fail("delete_node: nodeId kosong", 'Argument "nodeId" is required.');
1409
+ if (!nodeId) return fail("delete_node: nodeId missing", 'Argument "nodeId" is required.');
1339
1410
  const reason = readString(input, "reason");
1340
1411
  if (!reason) {
1341
1412
  return fail(
1342
- "delete_node: alasan kosong",
1413
+ "delete_node: reason missing",
1343
1414
  'Argument "reason" is required for destructive actions \u2014 state what the user asked for.'
1344
1415
  );
1345
1416
  }
@@ -1349,7 +1420,7 @@ var deleteNode = {
1349
1420
  if ("error" in applied) return applied.error;
1350
1421
  const op = { kind: "delete-node", nodeId };
1351
1422
  return succeed(
1352
- `Hapus ${describeNode(resolved.node)} \u2014 ${reason}`,
1423
+ `Delete ${describeNode(resolved.node)} \u2014 ${reason}`,
1353
1424
  { nodeId, removedType: resolved.node.type },
1354
1425
  { op, document: applied.document }
1355
1426
  );
@@ -1383,7 +1454,7 @@ var replaceNodeTool = {
1383
1454
  const reason = readString(input, "reason");
1384
1455
  if (!nodeId || !raw || !reason) {
1385
1456
  return fail(
1386
- "replace_node: argumen kurang",
1457
+ "replace_node: missing arguments",
1387
1458
  'Arguments "nodeId", "node" and "reason" are all required.'
1388
1459
  );
1389
1460
  }
@@ -1391,7 +1462,7 @@ var replaceNodeTool = {
1391
1462
  if ("error" in resolved) return resolved.error;
1392
1463
  const normalized = normalizeIncomingNode({ ...raw, id: void 0 }, context.document);
1393
1464
  if ("error" in normalized) {
1394
- return fail("replace_node: node tidak valid", normalized.error);
1465
+ return fail("replace_node: invalid node", normalized.error);
1395
1466
  }
1396
1467
  const replacement = { ...normalized.node, id: nodeId, type: resolved.node.type };
1397
1468
  const applied = applyCommand(
@@ -1403,7 +1474,7 @@ var replaceNodeTool = {
1403
1474
  if (violation) return violation;
1404
1475
  const op = { kind: "replace-node", nodeId, node: replacement };
1405
1476
  return succeed(
1406
- `Ganti struktur ${describeNode(resolved.node)} \u2014 ${reason}`,
1477
+ `Replace structure of ${describeNode(resolved.node)} \u2014 ${reason}`,
1407
1478
  { nodeId, childCount: replacement.children?.length ?? 0 },
1408
1479
  { op, document: applied.document }
1409
1480
  );
@@ -1482,7 +1553,7 @@ var KubuildAiAgent = class {
1482
1553
  const available = this.tools.map((t) => t.definition.name).join(", ");
1483
1554
  return {
1484
1555
  ok: false,
1485
- summary: `Tool "${call.name}" tidak dikenal`,
1556
+ summary: `Tool "${call.name}" is unknown`,
1486
1557
  snapshot,
1487
1558
  block: {
1488
1559
  type: "tool_result",
@@ -1579,7 +1650,7 @@ var KubuildAiAgent = class {
1579
1650
  selectedNodeId: request.selectedNodeId,
1580
1651
  prefix: this.options.systemPromptPrefix,
1581
1652
  stylePreference: request.stylePreference,
1582
- additionalContext: request.systemPrompt
1653
+ additionalContext: joinInstructions(request.instructions, request.systemPrompt)
1583
1654
  });
1584
1655
  const toolDefinitions = toToolDefinitions(this.tools);
1585
1656
  const messages = [...request.messages];
@@ -1658,11 +1729,11 @@ var KubuildAiAgent = class {
1658
1729
  }
1659
1730
  if (stoppedBy === "complete" && step >= maxSteps && !summary) {
1660
1731
  stoppedBy = "max-steps";
1661
- summary = `Berhenti setelah ${maxSteps} langkah. ${ops.length} perubahan sudah disiapkan \u2014 periksa hasilnya lalu minta lanjutan bila perlu.`;
1732
+ summary = `Stopped after ${maxSteps} steps. ${ops.length} change(s) prepared \u2014 review them and ask to continue if needed.`;
1662
1733
  this.log("warn", `[AGENT] hit maxSteps (${maxSteps}) with ${ops.length} op(s)`);
1663
1734
  }
1664
1735
  if (stoppedBy === "aborted" && !summary) {
1665
- summary = `Dihentikan. ${ops.length} perubahan sempat disiapkan.`;
1736
+ summary = `Stopped. ${ops.length} change(s) were prepared before stopping.`;
1666
1737
  }
1667
1738
  yield {
1668
1739
  type: "agent-complete",
@@ -1681,7 +1752,7 @@ var KubuildAiAgent = class {
1681
1752
  type: "agent-complete",
1682
1753
  result: {
1683
1754
  ops,
1684
- summary: summary || `Terjadi error: ${message}`,
1755
+ summary: summary || `An error occurred: ${message}`,
1685
1756
  stepsUsed: step,
1686
1757
  stoppedBy: "error",
1687
1758
  usage: { promptTokens, completionTokens }
@@ -1709,7 +1780,20 @@ var KubuildAiAgent = class {
1709
1780
  };
1710
1781
 
1711
1782
  // src/server/handler.ts
1712
- async function processAiRequest(engine, body, signal, agent) {
1783
+ var DEFAULT_MAX_CLIENT_INSTRUCTIONS_LENGTH = 4e3;
1784
+ function resolveClientInstructions(payload, options) {
1785
+ if (options?.allowClientInstructions === false) return void 0;
1786
+ const text = joinInstructions(
1787
+ typeof payload.instructions === "string" ? payload.instructions : void 0,
1788
+ typeof payload.systemPrompt === "string" ? payload.systemPrompt : void 0
1789
+ );
1790
+ if (!text) return void 0;
1791
+ const rawMax = options?.maxClientInstructionsLength;
1792
+ const max = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 0 ? Math.floor(rawMax) : DEFAULT_MAX_CLIENT_INSTRUCTIONS_LENGTH;
1793
+ if (max === 0) return void 0;
1794
+ return text.length > max ? text.slice(0, max) : text;
1795
+ }
1796
+ async function processAiRequest(engine, body, signal, agent, instructionsOptions) {
1713
1797
  if (!body || typeof body !== "object") {
1714
1798
  return {
1715
1799
  status: 400,
@@ -1724,6 +1808,7 @@ async function processAiRequest(engine, body, signal, agent) {
1724
1808
  }
1725
1809
  const payload = body;
1726
1810
  const mode = payload.mode || "full-page";
1811
+ const instructions = resolveClientInstructions(payload, instructionsOptions);
1727
1812
  if (mode === "full-page") {
1728
1813
  if (!payload.prompt || typeof payload.prompt !== "string") {
1729
1814
  return {
@@ -1746,7 +1831,8 @@ async function processAiRequest(engine, body, signal, agent) {
1746
1831
  metadata: payload.metadata,
1747
1832
  conversationHistory: payload.conversationHistory ?? payload.messages,
1748
1833
  sectionCount: payload.sectionCount,
1749
- plan: payload.plan
1834
+ plan: payload.plan,
1835
+ instructions
1750
1836
  },
1751
1837
  { signal }
1752
1838
  );
@@ -1773,7 +1859,8 @@ async function processAiRequest(engine, body, signal, agent) {
1773
1859
  prompt: payload.prompt,
1774
1860
  stylePreference: payload.stylePreference,
1775
1861
  targetSectionType: payload.targetSectionType,
1776
- parentContext: payload.parentContext
1862
+ parentContext: payload.parentContext,
1863
+ instructions
1777
1864
  },
1778
1865
  { signal }
1779
1866
  );
@@ -1799,7 +1886,8 @@ async function processAiRequest(engine, body, signal, agent) {
1799
1886
  {
1800
1887
  node: payload.node,
1801
1888
  instruction: payload.instruction,
1802
- stylePreference: payload.stylePreference
1889
+ stylePreference: payload.stylePreference,
1890
+ instructions
1803
1891
  },
1804
1892
  { signal }
1805
1893
  );
@@ -1825,7 +1913,8 @@ async function processAiRequest(engine, body, signal, agent) {
1825
1913
  {
1826
1914
  messages: payload.messages,
1827
1915
  currentDocument: payload.currentDocument,
1828
- selectedNodeId: payload.selectedNodeId
1916
+ selectedNodeId: payload.selectedNodeId,
1917
+ instructions
1829
1918
  },
1830
1919
  { signal }
1831
1920
  );
@@ -1845,7 +1934,8 @@ async function processAiRequest(engine, body, signal, agent) {
1845
1934
  document: payload.document,
1846
1935
  selectedNodeId: payload.selectedNodeId,
1847
1936
  stylePreference: payload.stylePreference,
1848
- maxSteps: payload.maxSteps
1937
+ maxSteps: payload.maxSteps,
1938
+ instructions
1849
1939
  },
1850
1940
  { signal }
1851
1941
  );
@@ -1874,7 +1964,8 @@ async function processAiRequest(engine, body, signal, agent) {
1874
1964
  tone: payload.tone,
1875
1965
  locale: payload.locale,
1876
1966
  sectionCount: payload.sectionCount,
1877
- conversationHistory: payload.conversationHistory ?? payload.messages
1967
+ conversationHistory: payload.conversationHistory ?? payload.messages,
1968
+ instructions
1878
1969
  },
1879
1970
  { signal }
1880
1971
  );
@@ -1998,8 +2089,10 @@ function createAiHandler(engine, options) {
1998
2089
  );
1999
2090
  }
2000
2091
  const body = await request.json().catch(() => null);
2001
- if (body && typeof body === "object" && body.stream === true) {
2002
- const mode = body.mode || "full-page";
2092
+ const streamMode = body && typeof body === "object" ? body.mode || "full-page" : null;
2093
+ if (body && typeof body === "object" && body.stream === true && (streamMode === "full-page" || streamMode === "chat" || streamMode === "agent")) {
2094
+ const mode = streamMode;
2095
+ const instructions = resolveClientInstructions(body, options);
2003
2096
  if (mode === "agent") {
2004
2097
  const validationError = validateAgentPayload(body, options?.agent);
2005
2098
  if (validationError) {
@@ -2015,7 +2108,8 @@ function createAiHandler(engine, options) {
2015
2108
  document: body.document,
2016
2109
  selectedNodeId: body.selectedNodeId,
2017
2110
  stylePreference: body.stylePreference,
2018
- maxSteps: body.maxSteps
2111
+ maxSteps: body.maxSteps,
2112
+ instructions
2019
2113
  },
2020
2114
  { signal: request.signal }
2021
2115
  ),
@@ -2043,7 +2137,8 @@ function createAiHandler(engine, options) {
2043
2137
  {
2044
2138
  messages: body.messages,
2045
2139
  currentDocument: body.currentDocument,
2046
- selectedNodeId: body.selectedNodeId
2140
+ selectedNodeId: body.selectedNodeId,
2141
+ instructions
2047
2142
  },
2048
2143
  { signal: request.signal }
2049
2144
  ),
@@ -2075,7 +2170,8 @@ function createAiHandler(engine, options) {
2075
2170
  metadata: body.metadata,
2076
2171
  conversationHistory: body.conversationHistory ?? body.messages,
2077
2172
  sectionCount: body.sectionCount,
2078
- plan: body.plan
2173
+ plan: body.plan,
2174
+ instructions
2079
2175
  },
2080
2176
  { signal: request.signal }
2081
2177
  ),
@@ -2086,7 +2182,8 @@ function createAiHandler(engine, options) {
2086
2182
  engine,
2087
2183
  body,
2088
2184
  request.signal,
2089
- options?.agent
2185
+ options?.agent,
2186
+ options
2090
2187
  );
2091
2188
  return new Response(JSON.stringify(response), {
2092
2189
  status,
@@ -2117,6 +2214,7 @@ function createAiHandler(engine, options) {
2117
2214
  export {
2118
2215
  AnthropicAdapter,
2119
2216
  CustomHttpAdapter,
2217
+ DEFAULT_MAX_CLIENT_INSTRUCTIONS_LENGTH,
2120
2218
  GeminiAdapter,
2121
2219
  KubuildAiAgent,
2122
2220
  KubuildAiEngine,
@@ -2130,6 +2228,7 @@ export {
2130
2228
  createDocumentTools,
2131
2229
  normalizeIncomingNode,
2132
2230
  processAiRequest,
2231
+ resolveClientInstructions,
2133
2232
  suggestNodeIds,
2134
2233
  toToolDefinitions
2135
2234
  };