bluekiwi 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/app-runtime/public/vs/components.css +74 -0
- package/dist/assets/app-runtime/public/vs/helper.js +222 -4
- package/dist/assets/mcp/server.js +170 -15
- package/dist/assets/skills/bk-design/SKILL.md +12 -0
- package/dist/assets/skills/bk-import/SKILL.md +9 -0
- package/dist/assets/skills/bk-improve/SKILL.md +12 -0
- package/package.json +1 -1
|
@@ -169,6 +169,80 @@ h2 {
|
|
|
169
169
|
pointer-events: none;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/* ─── Form Inputs ─── */
|
|
173
|
+
|
|
174
|
+
.bk-input,
|
|
175
|
+
.bk-textarea,
|
|
176
|
+
.bk-option-comment {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
gap: 0.5rem;
|
|
180
|
+
margin-top: 1rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.bk-field-label {
|
|
184
|
+
font-size: 0.85rem;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
color: var(--bk-text);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.bk-field-label .required {
|
|
190
|
+
color: var(--bk-danger);
|
|
191
|
+
margin-left: 0.25rem;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.bk-field-help {
|
|
195
|
+
font-size: 0.78rem;
|
|
196
|
+
color: var(--bk-text-muted);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.bk-input-control,
|
|
200
|
+
.bk-textarea-control {
|
|
201
|
+
width: 100%;
|
|
202
|
+
border: 1.5px solid var(--bk-border);
|
|
203
|
+
border-radius: var(--bk-radius);
|
|
204
|
+
background: var(--bk-surface);
|
|
205
|
+
color: var(--bk-text);
|
|
206
|
+
font: inherit;
|
|
207
|
+
padding: 0.8rem 0.9rem;
|
|
208
|
+
transition:
|
|
209
|
+
border-color 0.15s,
|
|
210
|
+
box-shadow 0.15s,
|
|
211
|
+
background 0.15s;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.bk-input-control::placeholder,
|
|
215
|
+
.bk-textarea-control::placeholder {
|
|
216
|
+
color: var(--bk-text-muted);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.bk-input-control:focus,
|
|
220
|
+
.bk-textarea-control:focus {
|
|
221
|
+
outline: none;
|
|
222
|
+
border-color: var(--bk-primary);
|
|
223
|
+
box-shadow: 0 0 0 3px rgba(65, 105, 225, 0.15);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.bk-textarea-control {
|
|
227
|
+
min-height: 7rem;
|
|
228
|
+
resize: vertical;
|
|
229
|
+
line-height: 1.55;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.bk-field-error {
|
|
233
|
+
font-size: 0.78rem;
|
|
234
|
+
color: var(--bk-danger);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.bk-option-comment[hidden] {
|
|
238
|
+
display: none;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.bk-option-comment {
|
|
242
|
+
border-top: 1px dashed var(--bk-border);
|
|
243
|
+
padding-top: 0.9rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
172
246
|
/* ─── 1. bk-options (A/B/C single select) ─── */
|
|
173
247
|
|
|
174
248
|
.bk-options {
|
|
@@ -6,13 +6,30 @@
|
|
|
6
6
|
(function () {
|
|
7
7
|
"use strict";
|
|
8
8
|
|
|
9
|
+
const COMMENTABLE_SELECTOR =
|
|
10
|
+
".bk-option, .bk-card, .bk-code-option, .bk-mockup-item, .bk-check-item";
|
|
11
|
+
|
|
9
12
|
/* ── i18n ── */
|
|
10
13
|
const UI_TEXT = {
|
|
11
|
-
ko: {
|
|
14
|
+
ko: {
|
|
15
|
+
submit: "확인",
|
|
16
|
+
submitted: "✓ 제출됨",
|
|
17
|
+
selected: "{n}개 선택됨",
|
|
18
|
+
detailsLabel: "추가 요청사항",
|
|
19
|
+
detailsPlaceholder: "선택 이유나 보완 요청을 적어주세요",
|
|
20
|
+
required: "필수",
|
|
21
|
+
requiredComment: "선택한 항목에 대한 코멘트를 입력해 주세요",
|
|
22
|
+
requiredField: "필수 입력 항목을 채워 주세요",
|
|
23
|
+
},
|
|
12
24
|
en: {
|
|
13
25
|
submit: "Submit",
|
|
14
26
|
submitted: "✓ Submitted",
|
|
15
27
|
selected: "{n} selected",
|
|
28
|
+
detailsLabel: "Additional details",
|
|
29
|
+
detailsPlaceholder: "Add your reasoning or requested changes",
|
|
30
|
+
required: "Required",
|
|
31
|
+
requiredComment: "Add a comment for the selected option",
|
|
32
|
+
requiredField: "Fill in the required fields",
|
|
16
33
|
},
|
|
17
34
|
};
|
|
18
35
|
const lang = document.documentElement.dataset.lang || "en";
|
|
@@ -32,6 +49,8 @@
|
|
|
32
49
|
bindSliders();
|
|
33
50
|
bindRanking();
|
|
34
51
|
bindMatrix();
|
|
52
|
+
bindFields();
|
|
53
|
+
bindOptionComments();
|
|
35
54
|
|
|
36
55
|
const btn = document.querySelector(".bk-vs-submit");
|
|
37
56
|
if (btn) {
|
|
@@ -60,6 +79,7 @@
|
|
|
60
79
|
});
|
|
61
80
|
if (!wasSelected) item.classList.add("selected");
|
|
62
81
|
hasInteracted = true;
|
|
82
|
+
syncConditionalFields();
|
|
63
83
|
updateSubmitState();
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -78,6 +98,7 @@
|
|
|
78
98
|
});
|
|
79
99
|
if (!wasSelected) item.classList.add("selected");
|
|
80
100
|
hasInteracted = true;
|
|
101
|
+
syncConditionalFields();
|
|
81
102
|
updateSubmitState();
|
|
82
103
|
});
|
|
83
104
|
});
|
|
@@ -99,9 +120,201 @@
|
|
|
99
120
|
function toggleCheck(item) {
|
|
100
121
|
item.classList.toggle("checked");
|
|
101
122
|
hasInteracted = true;
|
|
123
|
+
syncConditionalFields();
|
|
102
124
|
updateSubmitState();
|
|
103
125
|
}
|
|
104
126
|
|
|
127
|
+
function bindFields() {
|
|
128
|
+
document.querySelectorAll(".bk-textarea").forEach(function (container) {
|
|
129
|
+
if (container.querySelector("textarea")) return;
|
|
130
|
+
var textarea = document.createElement("textarea");
|
|
131
|
+
textarea.className = "bk-textarea-control";
|
|
132
|
+
textarea.name = container.dataset.name || "";
|
|
133
|
+
textarea.placeholder = container.dataset.placeholder || "";
|
|
134
|
+
textarea.rows = Number(container.dataset.rows || 4);
|
|
135
|
+
textarea.maxLength = container.dataset.maxlength
|
|
136
|
+
? Number(container.dataset.maxlength)
|
|
137
|
+
: -1;
|
|
138
|
+
mountField(container, textarea);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
document.querySelectorAll(".bk-input").forEach(function (container) {
|
|
142
|
+
if (container.querySelector("input")) return;
|
|
143
|
+
var input = document.createElement("input");
|
|
144
|
+
input.className = "bk-input-control";
|
|
145
|
+
input.name = container.dataset.name || "";
|
|
146
|
+
input.type = container.dataset.type || "text";
|
|
147
|
+
input.placeholder = container.dataset.placeholder || "";
|
|
148
|
+
input.maxLength = container.dataset.maxlength
|
|
149
|
+
? Number(container.dataset.maxlength)
|
|
150
|
+
: -1;
|
|
151
|
+
mountField(container, input);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function mountField(container, control) {
|
|
156
|
+
container.classList.add("bk-field");
|
|
157
|
+
|
|
158
|
+
if (container.dataset.label) {
|
|
159
|
+
var label = document.createElement("label");
|
|
160
|
+
label.className = "bk-field-label";
|
|
161
|
+
label.textContent = container.dataset.label;
|
|
162
|
+
if (container.hasAttribute("data-required")) {
|
|
163
|
+
var required = document.createElement("span");
|
|
164
|
+
required.className = "required";
|
|
165
|
+
required.textContent = "*";
|
|
166
|
+
label.appendChild(required);
|
|
167
|
+
}
|
|
168
|
+
container.appendChild(label);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (container.dataset.help) {
|
|
172
|
+
var help = document.createElement("div");
|
|
173
|
+
help.className = "bk-field-help";
|
|
174
|
+
help.textContent = container.dataset.help;
|
|
175
|
+
container.appendChild(help);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
control.addEventListener("click", function (e) {
|
|
179
|
+
e.stopPropagation();
|
|
180
|
+
});
|
|
181
|
+
control.addEventListener("input", function () {
|
|
182
|
+
hasInteracted = true;
|
|
183
|
+
updateSubmitState();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
container.appendChild(control);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function bindOptionComments() {
|
|
190
|
+
document.querySelectorAll(COMMENTABLE_SELECTOR).forEach(function (item) {
|
|
191
|
+
var needsComment =
|
|
192
|
+
item.hasAttribute("data-requires-comment") || !!item.dataset.commentName;
|
|
193
|
+
if (!needsComment || item.querySelector(".bk-option-comment")) return;
|
|
194
|
+
|
|
195
|
+
var wrapper = document.createElement("div");
|
|
196
|
+
wrapper.className = "bk-option-comment";
|
|
197
|
+
wrapper.hidden = true;
|
|
198
|
+
|
|
199
|
+
var label = document.createElement("label");
|
|
200
|
+
label.className = "bk-field-label";
|
|
201
|
+
label.textContent = item.dataset.commentLabel || t.detailsLabel;
|
|
202
|
+
if (item.hasAttribute("data-requires-comment")) {
|
|
203
|
+
var required = document.createElement("span");
|
|
204
|
+
required.className = "required";
|
|
205
|
+
required.textContent = "*";
|
|
206
|
+
label.appendChild(required);
|
|
207
|
+
}
|
|
208
|
+
wrapper.appendChild(label);
|
|
209
|
+
|
|
210
|
+
var textarea = document.createElement("textarea");
|
|
211
|
+
textarea.className = "bk-textarea-control";
|
|
212
|
+
textarea.placeholder =
|
|
213
|
+
item.dataset.commentPlaceholder || t.detailsPlaceholder;
|
|
214
|
+
textarea.rows = Number(item.dataset.commentRows || 3);
|
|
215
|
+
textarea.addEventListener("click", function (e) {
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
});
|
|
218
|
+
textarea.addEventListener("input", function () {
|
|
219
|
+
hasInteracted = true;
|
|
220
|
+
updateSubmitState();
|
|
221
|
+
});
|
|
222
|
+
wrapper.appendChild(textarea);
|
|
223
|
+
|
|
224
|
+
item.appendChild(wrapper);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
syncConditionalFields();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function syncConditionalFields() {
|
|
231
|
+
document.querySelectorAll(COMMENTABLE_SELECTOR).forEach(function (item) {
|
|
232
|
+
var comment = item.querySelector(".bk-option-comment");
|
|
233
|
+
if (!comment) return;
|
|
234
|
+
var selected =
|
|
235
|
+
item.classList.contains("selected") || item.classList.contains("checked");
|
|
236
|
+
comment.hidden = !selected;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function readControlValue(container) {
|
|
241
|
+
var control = container.querySelector("input, textarea");
|
|
242
|
+
if (!control) return "";
|
|
243
|
+
return String(control.value || "").trim();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function collectStandaloneFields(state) {
|
|
247
|
+
var fields = {};
|
|
248
|
+
var hasFields = false;
|
|
249
|
+
|
|
250
|
+
document.querySelectorAll(".bk-textarea, .bk-input").forEach(function (el) {
|
|
251
|
+
var name = el.dataset.name;
|
|
252
|
+
if (!name) return;
|
|
253
|
+
var value = readControlValue(el);
|
|
254
|
+
if (!value) return;
|
|
255
|
+
if (el.dataset.responseKey === "comment" || name === "comment") {
|
|
256
|
+
state.comment = value;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
fields[name] = value;
|
|
260
|
+
hasFields = true;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (hasFields) state.fields = fields;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function collectOptionComments(state) {
|
|
267
|
+
var optionComments = {};
|
|
268
|
+
var optionCommentCount = 0;
|
|
269
|
+
var fields = state.fields || {};
|
|
270
|
+
var hasFields = !!state.fields;
|
|
271
|
+
|
|
272
|
+
document.querySelectorAll(COMMENTABLE_SELECTOR).forEach(function (item) {
|
|
273
|
+
var selected =
|
|
274
|
+
item.classList.contains("selected") || item.classList.contains("checked");
|
|
275
|
+
if (!selected) return;
|
|
276
|
+
var commentWrap = item.querySelector(".bk-option-comment");
|
|
277
|
+
if (!commentWrap) return;
|
|
278
|
+
var textarea = commentWrap.querySelector("textarea");
|
|
279
|
+
var value = textarea ? String(textarea.value || "").trim() : "";
|
|
280
|
+
if (!value) return;
|
|
281
|
+
if (item.dataset.value) {
|
|
282
|
+
optionComments[item.dataset.value] = value;
|
|
283
|
+
optionCommentCount += 1;
|
|
284
|
+
}
|
|
285
|
+
if (item.dataset.commentName) {
|
|
286
|
+
fields[item.dataset.commentName] = value;
|
|
287
|
+
hasFields = true;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (optionCommentCount > 0) state.option_comments = optionComments;
|
|
292
|
+
if (hasFields) state.fields = fields;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getValidationError() {
|
|
296
|
+
var requiredFieldMissing = false;
|
|
297
|
+
document.querySelectorAll(".bk-textarea, .bk-input").forEach(function (el) {
|
|
298
|
+
if (!el.hasAttribute("data-required")) return;
|
|
299
|
+
if (!readControlValue(el)) requiredFieldMissing = true;
|
|
300
|
+
});
|
|
301
|
+
if (requiredFieldMissing) return t.requiredField;
|
|
302
|
+
|
|
303
|
+
var requiredCommentMissing = false;
|
|
304
|
+
document.querySelectorAll(COMMENTABLE_SELECTOR).forEach(function (item) {
|
|
305
|
+
var selected =
|
|
306
|
+
item.classList.contains("selected") || item.classList.contains("checked");
|
|
307
|
+
if (!selected || !item.hasAttribute("data-requires-comment")) return;
|
|
308
|
+
var commentWrap = item.querySelector(".bk-option-comment");
|
|
309
|
+
if (!commentWrap || !readControlValue(commentWrap)) {
|
|
310
|
+
requiredCommentMissing = true;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
if (requiredCommentMissing) return t.requiredComment;
|
|
314
|
+
|
|
315
|
+
return "";
|
|
316
|
+
}
|
|
317
|
+
|
|
105
318
|
/* ── Sliders ── */
|
|
106
319
|
function bindSliders() {
|
|
107
320
|
document.querySelectorAll(".bk-slider").forEach(function (container) {
|
|
@@ -345,6 +558,9 @@
|
|
|
345
558
|
});
|
|
346
559
|
if (hasMatrix) state.matrix = matrix;
|
|
347
560
|
|
|
561
|
+
collectStandaloneFields(state);
|
|
562
|
+
collectOptionComments(state);
|
|
563
|
+
|
|
348
564
|
return state;
|
|
349
565
|
}
|
|
350
566
|
|
|
@@ -365,11 +581,13 @@
|
|
|
365
581
|
function updateSubmitState() {
|
|
366
582
|
var btn = document.querySelector(".bk-vs-submit");
|
|
367
583
|
if (!btn || btn.classList.contains("submitted")) return;
|
|
368
|
-
|
|
584
|
+
var status = document.querySelector(".bk-vs-status");
|
|
585
|
+
var validationError = getValidationError();
|
|
369
586
|
var hasInteractable = document.querySelector(
|
|
370
|
-
".bk-options, .bk-cards, .bk-checklist, .bk-code-compare, .bk-slider, .bk-ranking, .bk-matrix, .bk-mockup-gallery, .bk-mockup-item",
|
|
587
|
+
".bk-options, .bk-cards, .bk-checklist, .bk-code-compare, .bk-slider, .bk-ranking, .bk-matrix, .bk-mockup-gallery, .bk-mockup-item, .bk-textarea, .bk-input",
|
|
371
588
|
);
|
|
372
|
-
btn.disabled = !(hasInteractable && hasInteracted);
|
|
589
|
+
btn.disabled = !(hasInteractable && hasInteracted) || !!validationError;
|
|
590
|
+
if (status) status.textContent = validationError;
|
|
373
591
|
}
|
|
374
592
|
|
|
375
593
|
/* ── Boot ── */
|
|
@@ -21228,9 +21228,16 @@ SELECTION (collect choices):
|
|
|
21228
21228
|
|
|
21229
21229
|
INPUT (collect values):
|
|
21230
21230
|
- bk-slider: Numeric. data-name, data-min, data-max, data-value, data-unit on .bk-slider. Contains label.
|
|
21231
|
+
- bk-input: Short text input. data-name, data-label, data-placeholder, optional data-required, and optional data-response-key="comment".
|
|
21232
|
+
- bk-textarea: Long-form memo field. data-name, data-label, data-placeholder, optional data-required, and optional data-response-key="comment".
|
|
21231
21233
|
- bk-ranking: Drag reorder. Wrap in .bk-ranking, each .bk-rank-item with data-value.
|
|
21232
21234
|
- bk-matrix: 2x2 placement. .bk-matrix with data-x-label, data-y-label. Each .bk-matrix-item with data-value.
|
|
21233
21235
|
|
|
21236
|
+
OPTION-LEVEL FEEDBACK:
|
|
21237
|
+
- Any selectable item (.bk-option, .bk-card, .bk-code-option, .bk-mockup-item, .bk-check-item) may add data-requires-comment to force a memo before submit.
|
|
21238
|
+
- Optional data-comment-name stores that memo into response.fields[data-comment-name].
|
|
21239
|
+
- Optional data-comment-label and data-comment-placeholder customize the inline memo UI.
|
|
21240
|
+
|
|
21234
21241
|
DISPLAY (no values):
|
|
21235
21242
|
- bk-split: Side-by-side. Two .bk-split-panel inside .bk-split.
|
|
21236
21243
|
- bk-pros-cons: .bk-pros + .bk-cons inside .bk-pros-cons.
|
|
@@ -21240,7 +21247,7 @@ DISPLAY (no values):
|
|
|
21240
21247
|
Layout: h2 for title, .bk-subtitle, .bk-section for breaks, .bk-label for category.
|
|
21241
21248
|
|
|
21242
21249
|
Response format (JSON via get_web_response):
|
|
21243
|
-
{selections: ["a"], values: {budget: 70}, ranking: ["security","ux"], matrix: {auth: {x:0.8,y:0.9}}}
|
|
21250
|
+
{selections: ["a"], values: {budget: 70}, ranking: ["security","ux"], matrix: {auth: {x:0.8,y:0.9}}, comment: "Tighten the pricing section", fields: {change_request: "Add rainy-day fallback"}, option_comments: {b: "Keep this option but improve lodging details"}}
|
|
21244
21251
|
Only populated fields are included.`,
|
|
21245
21252
|
{
|
|
21246
21253
|
task_id: { type: "number" },
|
|
@@ -21859,10 +21866,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
21859
21866
|
args
|
|
21860
21867
|
);
|
|
21861
21868
|
const createdId = created?.data?.id;
|
|
21869
|
+
let nodeVerification;
|
|
21870
|
+
if (createdId !== void 0 && Array.isArray(args.nodes)) {
|
|
21871
|
+
const expectedCount = args.nodes.length;
|
|
21872
|
+
const createdCount = Array.isArray(
|
|
21873
|
+
created?.data?.nodes
|
|
21874
|
+
) ? (created.data?.nodes ?? []).length : 0;
|
|
21875
|
+
const verified = await client.request(
|
|
21876
|
+
"GET",
|
|
21877
|
+
`/api/workflows/${createdId}`
|
|
21878
|
+
);
|
|
21879
|
+
const verifiedCount = Array.isArray(verified?.data?.nodes) ? verified.data.nodes.length : 0;
|
|
21880
|
+
nodeVerification = {
|
|
21881
|
+
expected_count: expectedCount,
|
|
21882
|
+
created_count: createdCount,
|
|
21883
|
+
verified_count: verifiedCount,
|
|
21884
|
+
mismatch: createdCount !== expectedCount || verifiedCount !== expectedCount
|
|
21885
|
+
};
|
|
21886
|
+
}
|
|
21862
21887
|
return wrap({
|
|
21863
21888
|
...created,
|
|
21864
21889
|
...createdId !== void 0 && {
|
|
21865
21890
|
webui_url: `${apiUrl.replace(/\/$/, "")}/workflows/${createdId}`
|
|
21891
|
+
},
|
|
21892
|
+
...nodeVerification && {
|
|
21893
|
+
node_verification: nodeVerification
|
|
21866
21894
|
}
|
|
21867
21895
|
});
|
|
21868
21896
|
}
|
|
@@ -21884,13 +21912,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
21884
21912
|
const workflowId = requireNumberArg(args, "workflow_id");
|
|
21885
21913
|
const body = { ...args };
|
|
21886
21914
|
delete body.workflow_id;
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
|
|
21892
|
-
|
|
21915
|
+
logNodeMutationAudit("append_node.request", {
|
|
21916
|
+
workflow_id: workflowId,
|
|
21917
|
+
payload: sanitizeNodeMutationPayload(body)
|
|
21918
|
+
});
|
|
21919
|
+
const beforeNodes = await fetchWorkflowNodeSnapshot(workflowId);
|
|
21920
|
+
const appended = await client.request(
|
|
21921
|
+
"POST",
|
|
21922
|
+
`/api/workflows/${workflowId}/nodes`,
|
|
21923
|
+
body
|
|
21893
21924
|
);
|
|
21925
|
+
const afterNodes = await fetchWorkflowNodeSnapshot(workflowId);
|
|
21926
|
+
const verification = buildNodeMutationVerification({
|
|
21927
|
+
mutation: "append_node",
|
|
21928
|
+
beforeNodes,
|
|
21929
|
+
afterNodes,
|
|
21930
|
+
expectedDelta: 1,
|
|
21931
|
+
expectedNodeId: appended?.data?.id ?? null
|
|
21932
|
+
});
|
|
21933
|
+
logNodeMutationAudit("append_node.verification", {
|
|
21934
|
+
workflow_id: workflowId,
|
|
21935
|
+
verification
|
|
21936
|
+
});
|
|
21937
|
+
if (verification.mismatch) {
|
|
21938
|
+
logNodeMutationAudit("append_node.mismatch", {
|
|
21939
|
+
workflow_id: workflowId,
|
|
21940
|
+
payload: sanitizeNodeMutationPayload(body),
|
|
21941
|
+
verification
|
|
21942
|
+
});
|
|
21943
|
+
throw new Error(
|
|
21944
|
+
`append_node verification failed for workflow ${workflowId}: ${JSON.stringify(verification)}`
|
|
21945
|
+
);
|
|
21946
|
+
}
|
|
21947
|
+
return wrap({
|
|
21948
|
+
...appended,
|
|
21949
|
+
node_verification: verification
|
|
21950
|
+
});
|
|
21894
21951
|
}
|
|
21895
21952
|
case "insert_node": {
|
|
21896
21953
|
const workflowId = requireNumberArg(args, "workflow_id");
|
|
@@ -21898,13 +21955,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
21898
21955
|
const body = { ...args };
|
|
21899
21956
|
delete body.workflow_id;
|
|
21900
21957
|
delete body.after_step;
|
|
21901
|
-
|
|
21902
|
-
|
|
21903
|
-
|
|
21904
|
-
|
|
21905
|
-
|
|
21906
|
-
|
|
21958
|
+
logNodeMutationAudit("insert_node.request", {
|
|
21959
|
+
workflow_id: workflowId,
|
|
21960
|
+
after_step: afterStep,
|
|
21961
|
+
payload: sanitizeNodeMutationPayload(body)
|
|
21962
|
+
});
|
|
21963
|
+
const beforeNodes = await fetchWorkflowNodeSnapshot(workflowId);
|
|
21964
|
+
const inserted = await client.request(
|
|
21965
|
+
"POST",
|
|
21966
|
+
`/api/workflows/${workflowId}/nodes?after=${afterStep}`,
|
|
21967
|
+
body
|
|
21907
21968
|
);
|
|
21969
|
+
const afterNodes = await fetchWorkflowNodeSnapshot(workflowId);
|
|
21970
|
+
const verification = buildNodeMutationVerification({
|
|
21971
|
+
mutation: "insert_node",
|
|
21972
|
+
beforeNodes,
|
|
21973
|
+
afterNodes,
|
|
21974
|
+
expectedDelta: 1,
|
|
21975
|
+
expectedNodeId: inserted?.data?.id ?? null
|
|
21976
|
+
});
|
|
21977
|
+
logNodeMutationAudit("insert_node.verification", {
|
|
21978
|
+
workflow_id: workflowId,
|
|
21979
|
+
after_step: afterStep,
|
|
21980
|
+
verification
|
|
21981
|
+
});
|
|
21982
|
+
if (verification.mismatch) {
|
|
21983
|
+
logNodeMutationAudit("insert_node.mismatch", {
|
|
21984
|
+
workflow_id: workflowId,
|
|
21985
|
+
after_step: afterStep,
|
|
21986
|
+
payload: sanitizeNodeMutationPayload(body),
|
|
21987
|
+
verification
|
|
21988
|
+
});
|
|
21989
|
+
throw new Error(
|
|
21990
|
+
`insert_node verification failed for workflow ${workflowId}: ${JSON.stringify(verification)}`
|
|
21991
|
+
);
|
|
21992
|
+
}
|
|
21993
|
+
return wrap({
|
|
21994
|
+
...inserted,
|
|
21995
|
+
node_verification: verification
|
|
21996
|
+
});
|
|
21908
21997
|
}
|
|
21909
21998
|
case "update_node": {
|
|
21910
21999
|
const workflowId = requireNumberArg(args, "workflow_id");
|
|
@@ -21915,7 +22004,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
21915
22004
|
return wrap(
|
|
21916
22005
|
await client.request(
|
|
21917
22006
|
"PATCH",
|
|
21918
|
-
`/api/workflows/${workflowId}/
|
|
22007
|
+
`/api/workflows/${workflowId}/nodes/${nodeId}`,
|
|
21919
22008
|
body
|
|
21920
22009
|
)
|
|
21921
22010
|
);
|
|
@@ -21926,7 +22015,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
21926
22015
|
return wrap(
|
|
21927
22016
|
await client.request(
|
|
21928
22017
|
"DELETE",
|
|
21929
|
-
`/api/workflows/${workflowId}/
|
|
22018
|
+
`/api/workflows/${workflowId}/nodes/${nodeId}`
|
|
21930
22019
|
)
|
|
21931
22020
|
);
|
|
21932
22021
|
}
|
|
@@ -22184,6 +22273,72 @@ function wrapError(message) {
|
|
|
22184
22273
|
isError: true
|
|
22185
22274
|
};
|
|
22186
22275
|
}
|
|
22276
|
+
function logNodeMutationAudit(event, data) {
|
|
22277
|
+
const record2 = {
|
|
22278
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22279
|
+
scope: "node-mutation-audit",
|
|
22280
|
+
event,
|
|
22281
|
+
...data
|
|
22282
|
+
};
|
|
22283
|
+
console.error(JSON.stringify(record2));
|
|
22284
|
+
}
|
|
22285
|
+
function sanitizeNodeMutationPayload(payload) {
|
|
22286
|
+
const out = {};
|
|
22287
|
+
const allowedKeys = [
|
|
22288
|
+
"title",
|
|
22289
|
+
"node_type",
|
|
22290
|
+
"instruction",
|
|
22291
|
+
"instruction_id",
|
|
22292
|
+
"credential_id",
|
|
22293
|
+
"hitl",
|
|
22294
|
+
"visual_selection",
|
|
22295
|
+
"loop_back_to"
|
|
22296
|
+
];
|
|
22297
|
+
for (const key of allowedKeys) {
|
|
22298
|
+
if (key in payload) {
|
|
22299
|
+
out[key] = payload[key];
|
|
22300
|
+
}
|
|
22301
|
+
}
|
|
22302
|
+
if (typeof out["instruction"] === "string") {
|
|
22303
|
+
out["instruction_preview"] = String(out["instruction"]).slice(0, 160);
|
|
22304
|
+
delete out["instruction"];
|
|
22305
|
+
}
|
|
22306
|
+
return out;
|
|
22307
|
+
}
|
|
22308
|
+
async function fetchWorkflowNodeSnapshot(workflowId) {
|
|
22309
|
+
const workflow = await client.request("GET", `/api/workflows/${workflowId}`);
|
|
22310
|
+
const nodes = Array.isArray(workflow?.data?.nodes) ? workflow.data.nodes : [];
|
|
22311
|
+
return nodes.map((node) => ({
|
|
22312
|
+
id: Number(node["id"]),
|
|
22313
|
+
step_order: typeof node["step_order"] === "number" ? node["step_order"] : null,
|
|
22314
|
+
title: typeof node["title"] === "string" ? node["title"] : null,
|
|
22315
|
+
node_type: typeof node["node_type"] === "string" ? node["node_type"] : null
|
|
22316
|
+
}));
|
|
22317
|
+
}
|
|
22318
|
+
function buildNodeMutationVerification(input) {
|
|
22319
|
+
const beforeIds = new Set(input.beforeNodes.map((node) => node.id));
|
|
22320
|
+
const addedNodes = input.afterNodes.filter((node) => !beforeIds.has(node.id));
|
|
22321
|
+
const expectedCount = input.beforeNodes.length + input.expectedDelta;
|
|
22322
|
+
const countDelta = input.afterNodes.length - input.beforeNodes.length;
|
|
22323
|
+
const mismatch = input.afterNodes.length !== expectedCount || countDelta !== input.expectedDelta || addedNodes.length !== input.expectedDelta || input.expectedNodeId !== null && !addedNodes.some((node) => node.id === input.expectedNodeId);
|
|
22324
|
+
return {
|
|
22325
|
+
mutation: input.mutation,
|
|
22326
|
+
before_count: input.beforeNodes.length,
|
|
22327
|
+
after_count: input.afterNodes.length,
|
|
22328
|
+
expected_count: expectedCount,
|
|
22329
|
+
delta: countDelta,
|
|
22330
|
+
expected_delta: input.expectedDelta,
|
|
22331
|
+
expected_node_id: input.expectedNodeId,
|
|
22332
|
+
added_node_ids: addedNodes.map((node) => node.id),
|
|
22333
|
+
added_nodes: addedNodes.map((node) => ({
|
|
22334
|
+
id: node.id,
|
|
22335
|
+
step_order: node.step_order,
|
|
22336
|
+
title: node.title,
|
|
22337
|
+
node_type: node.node_type
|
|
22338
|
+
})),
|
|
22339
|
+
mismatch
|
|
22340
|
+
};
|
|
22341
|
+
}
|
|
22187
22342
|
async function scanRepoLocal(args) {
|
|
22188
22343
|
const scanPath = args["path"];
|
|
22189
22344
|
if (typeof scanPath !== "string" || scanPath.trim().length === 0) {
|
|
@@ -108,6 +108,15 @@ Call `create_workflow`:
|
|
|
108
108
|
}
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
<HARD-RULE>
|
|
112
|
+
Immediately validate the `create_workflow` result before doing anything else:
|
|
113
|
+
|
|
114
|
+
- If the response includes `node_verification`, require `node_verification.mismatch === false`.
|
|
115
|
+
- If `nodes` were supplied to `create_workflow`, treat that call as the only allowed initial node creation step.
|
|
116
|
+
- Never append the same planned nodes after `create_workflow(nodes=[...])` because an older server response showed `data.nodes=[]`.
|
|
117
|
+
- If verification fails, STOP and investigate. Do not continue with `append_node` or `insert_node`.
|
|
118
|
+
</HARD-RULE>
|
|
119
|
+
|
|
111
120
|
### Step 6: Report Result + Open in Browser
|
|
112
121
|
|
|
113
122
|
On success, open the workflow detail page in the browser:
|
|
@@ -336,11 +345,14 @@ Place two mockup cards side by side using display:grid;grid-template-columns:1fr
|
|
|
336
345
|
## Node Modification Strategy
|
|
337
346
|
|
|
338
347
|
<HARD-RULE>
|
|
348
|
+
- Before any structural edit, refresh the current workflow structure from the server and use the latest node ids / step orders as the source of truth.
|
|
339
349
|
- Update a single node → `update_node(workflow_id, node_id, ...only changed fields)`
|
|
340
350
|
- Append a node (at the end) → `append_node(workflow_id, title, instruction, node_type, loop_back_to?, visual_selection?)`
|
|
341
351
|
- Insert a node (in the middle) → `insert_node(workflow_id, after_step=N, title, instruction, node_type, loop_back_to?, visual_selection?)`
|
|
342
352
|
- Delete a node → `remove_node(workflow_id, node_id)`
|
|
343
353
|
- Never use `update_workflow(nodes=[...])` for full replacement unless a complete redesign is intended
|
|
354
|
+
- After every `append_node` or `insert_node`, inspect the returned `node_verification`.
|
|
355
|
+
- If `node_verification.mismatch === true`, STOP immediately and do not continue issuing more structural edits from the current plan snapshot.
|
|
344
356
|
</HARD-RULE>
|
|
345
357
|
|
|
346
358
|
## Inline vs Template Instructions
|
|
@@ -191,6 +191,15 @@ Call `create_workflow`:
|
|
|
191
191
|
}
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
+
<HARD-RULE>
|
|
195
|
+
Immediately validate the `create_workflow` result:
|
|
196
|
+
|
|
197
|
+
- If the response includes `node_verification`, require `node_verification.mismatch === false`.
|
|
198
|
+
- If `nodes` were supplied to `create_workflow`, do not follow with `append_node` for the same imported step set.
|
|
199
|
+
- Never treat an older `data.nodes=[]` response shape as evidence that the initial nodes were not created.
|
|
200
|
+
- If verification fails, STOP and investigate instead of patching the workflow incrementally.
|
|
201
|
+
</HARD-RULE>
|
|
202
|
+
|
|
194
203
|
## Step 8: Report Result + Open in Browser
|
|
195
204
|
|
|
196
205
|
On success:
|
|
@@ -119,6 +119,15 @@ remove_node(workflow_id=67, node_id=115)
|
|
|
119
119
|
When editing in place, apply each change as a separate `update_node`, `insert_node`, `append_node`, or `remove_node` call. Never use `update_workflow(nodes=[...])` for in-place edits — it replaces ALL nodes and loses node IDs.
|
|
120
120
|
</HARD-RULE>
|
|
121
121
|
|
|
122
|
+
<HARD-RULE>
|
|
123
|
+
Before any structural edit session:
|
|
124
|
+
|
|
125
|
+
- Refresh the current workflow structure from the server and use the latest node ids / step orders as the source of truth.
|
|
126
|
+
- Do not rely on a stale node snapshot captured before other edits or user actions.
|
|
127
|
+
- After every `append_node` or `insert_node`, inspect the returned `node_verification`.
|
|
128
|
+
- If `node_verification.mismatch === true`, STOP immediately and do not continue applying edits from the current plan.
|
|
129
|
+
</HARD-RULE>
|
|
130
|
+
|
|
122
131
|
After all changes, report:
|
|
123
132
|
|
|
124
133
|
```
|
|
@@ -186,9 +195,12 @@ When improving a node with `visual_selection: true`:
|
|
|
186
195
|
## Node Modification Strategy
|
|
187
196
|
|
|
188
197
|
<HARD-RULE>
|
|
198
|
+
- Before any structural edit, refresh the current workflow structure from the server and use the latest node ids / step orders as the source of truth.
|
|
189
199
|
- Update a single node → `update_node(workflow_id, node_id, ...only changed fields)`
|
|
190
200
|
- Append a node (at the end) → `append_node(workflow_id, title, instruction, node_type, loop_back_to?, visual_selection?)`
|
|
191
201
|
- Insert a node (in the middle) → `insert_node(workflow_id, after_step=N, title, instruction, node_type, loop_back_to?, visual_selection?)`
|
|
192
202
|
- Delete a node → `remove_node(workflow_id, node_id)`
|
|
193
203
|
- Never use `update_workflow(nodes=[...])` for full replacement unless a complete redesign is intended
|
|
204
|
+
- After every `append_node` or `insert_node`, inspect the returned `node_verification`.
|
|
205
|
+
- If `node_verification.mismatch === true`, STOP immediately and do not continue issuing more structural edits from the current plan snapshot.
|
|
194
206
|
</HARD-RULE>
|