openbot 0.2.10 → 0.2.11

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,186 +1,99 @@
1
1
  import { generateId } from "melony";
2
- import { ui } from "@melony/ui-kit/server";
2
+ import { uiEvent } from "../../ui/block.js";
3
3
  import { widgets } from "../../ui/widgets/index.js";
4
- const DEFAULT_REDACTED_KEY_PATTERNS = [
5
- /toolcallid/i,
6
- /content/i,
7
- /stdout/i,
8
- /stderr/i,
9
- /password/i,
10
- /secret/i,
11
- /token/i,
12
- /api[_-]?key/i,
13
- /authorization/i,
14
- /cookie/i,
15
- ];
16
- const MAX_VALUE_LENGTH = 240;
17
- const MAX_DETAILS = 8;
18
- function serializeValue(value) {
19
- if (value === undefined || value === null)
20
- return "-";
21
- const serialized = typeof value === "string"
22
- ? value
23
- : typeof value === "number" || typeof value === "boolean"
24
- ? String(value)
25
- : JSON.stringify(value);
26
- if (serialized.length <= MAX_VALUE_LENGTH)
27
- return serialized;
28
- return `${serialized.slice(0, MAX_VALUE_LENGTH - 3)}...`;
29
- }
30
- function buildActionLabel(eventType) {
31
- const action = eventType.startsWith("action:") ? eventType.slice("action:".length) : eventType;
32
- return action.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
33
- }
34
- function toTitleCaseKey(key) {
35
- return key
36
- .replace(/([A-Z])/g, " $1")
37
- .replace(/[_-]+/g, " ")
38
- .replace(/^./, (c) => c.toUpperCase())
39
- .trim();
40
- }
41
- function isRedactedKey(key, hiddenKeys = []) {
42
- if (hiddenKeys.includes(key))
43
- return true;
44
- return DEFAULT_REDACTED_KEY_PATTERNS.some((pattern) => pattern.test(key));
45
- }
46
- function sanitizePayload(value, hiddenKeys = []) {
47
- if (Array.isArray(value)) {
48
- return value.map((item) => sanitizePayload(item, hiddenKeys));
49
- }
50
- if (value && typeof value === "object") {
51
- const obj = value;
52
- const sanitizedEntries = Object.entries(obj).map(([key, v]) => {
53
- if (isRedactedKey(key, hiddenKeys))
54
- return [key, "[REDACTED]"];
55
- return [key, sanitizePayload(v, hiddenKeys)];
56
- });
57
- return Object.fromEntries(sanitizedEntries);
58
- }
59
- if (typeof value === "string" && value.length > 1000) {
60
- return `${value.slice(0, 997)}...`;
61
- }
62
- return value;
63
- }
64
- function summarizeData(data = {}, hiddenKeys = []) {
65
- const safeData = sanitizePayload(data, hiddenKeys);
66
- return JSON.stringify(safeData, null, 2);
67
- }
68
- function isRenderableDetailValue(value) {
69
- return value !== undefined && value !== null;
70
- }
71
- function deriveDetailEntries(data, rule) {
72
- const hiddenKeys = rule.hiddenKeys ?? [];
73
- if (rule.detailKeys?.length) {
74
- return rule.detailKeys
75
- .filter((key) => key in data)
76
- .filter((key) => !isRedactedKey(key, hiddenKeys))
77
- .filter((key) => isRenderableDetailValue(data[key]))
78
- .map((key) => ({
79
- label: toTitleCaseKey(key),
80
- value: serializeValue(data[key]),
81
- }))
82
- .slice(0, MAX_DETAILS);
83
- }
84
- return Object.entries(data)
85
- .filter(([key]) => !isRedactedKey(key, hiddenKeys))
86
- .filter(([_, value]) => isRenderableDetailValue(value))
87
- .slice(0, MAX_DETAILS)
88
- .map(([key, value]) => ({
89
- label: toTitleCaseKey(key),
90
- value: serializeValue(value),
91
- }));
4
+ function getActionName(eventType) {
5
+ return eventType.startsWith("action:")
6
+ ? eventType.slice("action:".length)
7
+ : eventType;
92
8
  }
93
9
  function buildApprovalData(event, rule) {
94
- const eventType = event.type;
95
- const data = (event.data ?? {});
96
- const details = [
97
- { label: "Action", value: buildActionLabel(eventType) },
98
- { label: "Event", value: eventType },
99
- ...deriveDetailEntries(data, rule),
100
- ];
10
+ const actionName = getActionName(String(event.type));
11
+ const toolCallId = event?.data?.toolCallId;
12
+ const data = event?.data ?? {};
101
13
  return {
102
- summary: rule.message || "The agent wants to execute an action. Review details before approving.",
103
- details,
104
- rawPayload: summarizeData(data, rule.hiddenKeys ?? []),
14
+ summary: rule.message ??
15
+ "The agent requested a protected action. Approve to continue or deny to block it.",
16
+ details: [
17
+ { label: "Action", value: actionName },
18
+ ...(toolCallId ? [{ label: "Tool call", value: String(toolCallId) }] : []),
19
+ ],
20
+ rawPayload: JSON.stringify(data, null, 2),
105
21
  };
106
22
  }
107
23
  /**
108
- * Approval Plugin for OpenBot.
109
- * Intercepts specific actions and requires user approval before proceeding.
110
- * Optimized using the new melony intercept() feature.
24
+ * Minimal approval gate:
25
+ * - Intercept protected action events.
26
+ * - Suspend current request and show Approve/Deny UI.
27
+ * - Resume only when user sends action:approve or action:deny.
111
28
  */
112
29
  export const approvalPlugin = (options) => (builder) => {
113
- const { rules = [] } = options;
114
- // Register an interceptor that runs before any handlers.
115
- // This is the correct way to handle HITL/Approval in Melony.
30
+ const rules = options?.rules ?? [];
116
31
  builder.intercept(async (event, { state, suspend }) => {
117
- // Skip if already approved or if it's an internal approval event
118
- // We cast event to any to access the meta property which is used for internal state tracking
119
- const meta = event.meta;
120
- if (meta?.approved ||
121
- event.type === "action:approve" ||
122
- event.type === "action:deny" ||
123
- event.type === "ui" ||
124
- event.type.endsWith(":status")) {
32
+ const type = String(event.type ?? "");
33
+ const meta = event.meta ?? {};
34
+ // Never intercept internal approval events or already-approved replays.
35
+ if (type === "action:approve" || type === "action:deny" || meta.approved === true) {
125
36
  return;
126
37
  }
127
- const rule = rules.find(r => event.type.startsWith(r.action));
38
+ const rule = rules.find((r) => type.startsWith(r.action));
128
39
  if (!rule)
129
40
  return;
130
41
  const approvalId = `approve_${generateId()}`;
131
- if (!state.pendingApprovals) {
132
- state.pendingApprovals = {};
133
- }
134
- state.pendingApprovals[approvalId] = event;
135
- // Use suspend(event) to emit the UI and halt execution of any handlers for this event.
136
- // This effectively "pauses" the run for user input.
137
- const approvalData = buildApprovalData(event, rule);
138
- suspend(ui.event(widgets.approvalCard("Approval Required", approvalData, {
139
- type: "action:approve",
140
- data: { id: approvalId }
141
- }, {
142
- type: "action:deny",
143
- data: { id: approvalId }
144
- })));
42
+ state.pendingApprovals ?? (state.pendingApprovals = {});
43
+ state.pendingApprovals[approvalId] = {
44
+ originalEvent: event,
45
+ createdAt: Date.now(),
46
+ };
47
+ suspend({
48
+ type: "suspend",
49
+ data: {
50
+ reason: "approval",
51
+ id: approvalId,
52
+ event: uiEvent(widgets.approvalCard("Approval Required", buildApprovalData(event, rule), { type: "action:approve", data: { id: approvalId } }, { type: "action:deny", data: { id: approvalId } }, { placement: "attention", id: approvalId })),
53
+ },
54
+ });
145
55
  });
146
- // Handle Approval response from user
147
56
  builder.on("action:approve", async function* (event, { state }) {
148
- const { id } = event.data;
149
- const originalEvent = state.pendingApprovals?.[id];
150
- if (originalEvent) {
151
- delete state.pendingApprovals[id];
152
- yield ui.event(widgets.status("Action approved", "success"));
153
- // Re-emit the original event with approved: true.
154
- // The interceptor will see it, but bypass because of meta.approved.
155
- // Then the appropriate handlers for the event will finally run.
156
- yield {
157
- ...originalEvent,
158
- meta: {
159
- ...originalEvent.meta,
160
- approved: true,
161
- },
162
- };
57
+ const id = event?.data?.id;
58
+ if (!id)
59
+ return;
60
+ const pending = state.pendingApprovals?.[id];
61
+ if (!pending) {
62
+ return;
163
63
  }
64
+ delete state.pendingApprovals[id];
65
+ // Re-emit the original action with approval marker.
66
+ yield {
67
+ ...pending.originalEvent,
68
+ meta: {
69
+ ...(pending.originalEvent?.meta ?? {}),
70
+ approved: true,
71
+ },
72
+ };
164
73
  });
165
- // Handle Denial response from user
166
74
  builder.on("action:deny", async function* (event, { state }) {
167
- const { id } = event.data;
168
- const originalEvent = state.pendingApprovals?.[id];
169
- if (originalEvent) {
170
- delete state.pendingApprovals[id];
171
- yield ui.event(widgets.status("Action denied", "error"));
172
- // If it was a tool call (action:*), return a result error so the LLM knows it failed
173
- if (originalEvent.data?.toolCallId) {
174
- yield {
175
- type: "action:result",
176
- data: {
177
- action: originalEvent.type.replace("action:", ""),
178
- toolCallId: originalEvent.data.toolCallId,
179
- result: { error: "Action denied by user" },
180
- success: false,
181
- },
182
- };
183
- }
75
+ const id = event?.data?.id;
76
+ if (!id)
77
+ return;
78
+ const pending = state.pendingApprovals?.[id];
79
+ if (!pending) {
80
+ return;
81
+ }
82
+ delete state.pendingApprovals[id];
83
+ yield uiEvent(widgets.status("Action denied", "error", { placement: "attention", id }));
84
+ const originalEvent = pending.originalEvent;
85
+ const toolCallId = originalEvent?.data?.toolCallId;
86
+ if (toolCallId) {
87
+ yield {
88
+ type: "action:result",
89
+ data: {
90
+ action: getActionName(String(originalEvent.type ?? "")),
91
+ toolCallId,
92
+ result: { error: "Action denied by user", denied: true },
93
+ success: false,
94
+ halt: true,
95
+ },
96
+ };
184
97
  }
185
98
  });
186
99
  };
@@ -1,4 +1,4 @@
1
- import { ui } from "@melony/ui-kit/server";
1
+ import { uiEvent } from "../../ui/block.js";
2
2
  import { z } from "zod";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
@@ -166,6 +166,6 @@ export const fileSystemPlugin = (options = {}) => (builder) => {
166
166
  }
167
167
  });
168
168
  builder.on("file-system:status", async function* (event) {
169
- yield ui.event(statusWidget(event.data.message, event.data.severity));
169
+ yield uiEvent(statusWidget(event.data.message, event.data.severity));
170
170
  });
171
171
  };
@@ -0,0 +1,139 @@
1
+ function truncateMiddle(value, maxChars) {
2
+ if (value.length <= maxChars)
3
+ return value;
4
+ const half = Math.floor(maxChars / 2);
5
+ const removed = value.length - maxChars;
6
+ return `${value.slice(0, half)}\n\n[... ${removed} characters truncated ...]\n\n${value.slice(-half)}`;
7
+ }
8
+ function estimateStringTokens(value) {
9
+ if (!value)
10
+ return 0;
11
+ return Math.max(1, Math.ceil(value.length / 4));
12
+ }
13
+ function estimateMessageTokens(message) {
14
+ if (typeof message.content === "string") {
15
+ return estimateStringTokens(message.content);
16
+ }
17
+ if (Array.isArray(message.content)) {
18
+ let total = 0;
19
+ for (const part of message.content) {
20
+ if (typeof part === "string") {
21
+ total += estimateStringTokens(part);
22
+ }
23
+ else if (part && typeof part === "object") {
24
+ total += estimateStringTokens(JSON.stringify(part));
25
+ }
26
+ }
27
+ return total;
28
+ }
29
+ return 0;
30
+ }
31
+ function cloneWithTrimmedSystemMessages(messages, maxSystemChars) {
32
+ return messages.map((message) => {
33
+ if (message.role !== "system" || typeof message.content !== "string") {
34
+ return message;
35
+ }
36
+ return {
37
+ ...message,
38
+ content: truncateMiddle(message.content, maxSystemChars),
39
+ };
40
+ });
41
+ }
42
+ function getLeadingSystemCount(messages) {
43
+ let count = 0;
44
+ for (const message of messages) {
45
+ if (message.role !== "system")
46
+ break;
47
+ count++;
48
+ }
49
+ return count;
50
+ }
51
+ function collectAssistantToolPairings(messages) {
52
+ const toolCallToAssistant = new Map();
53
+ const assistantToToolResults = new Map();
54
+ messages.forEach((message, index) => {
55
+ if (message.role !== "assistant" || !Array.isArray(message.content))
56
+ return;
57
+ for (const part of message.content) {
58
+ if (part
59
+ && typeof part === "object"
60
+ && part.type === "tool-call"
61
+ && typeof part.toolCallId === "string") {
62
+ toolCallToAssistant.set(part.toolCallId, index);
63
+ }
64
+ }
65
+ });
66
+ messages.forEach((message, index) => {
67
+ if (message.role !== "tool" || !Array.isArray(message.content))
68
+ return;
69
+ for (const part of message.content) {
70
+ if (!part || typeof part !== "object")
71
+ continue;
72
+ const toolCallId = part.toolCallId;
73
+ if (typeof toolCallId !== "string")
74
+ continue;
75
+ const assistantIndex = toolCallToAssistant.get(toolCallId);
76
+ if (typeof assistantIndex !== "number")
77
+ continue;
78
+ const bucket = assistantToToolResults.get(assistantIndex) ?? new Set();
79
+ bucket.add(index);
80
+ assistantToToolResults.set(assistantIndex, bucket);
81
+ }
82
+ });
83
+ return assistantToToolResults;
84
+ }
85
+ function buildAtomicGroupIndices(index, messages, assistantToToolResults) {
86
+ const message = messages[index];
87
+ if (!message)
88
+ return [];
89
+ if (message.role === "assistant") {
90
+ const results = assistantToToolResults.get(index);
91
+ if (!results || results.size === 0)
92
+ return [index];
93
+ return [index, ...Array.from(results)].sort((a, b) => a - b);
94
+ }
95
+ if (message.role === "tool") {
96
+ for (const [assistantIndex, resultIndices] of assistantToToolResults.entries()) {
97
+ if (resultIndices.has(index)) {
98
+ return [assistantIndex, ...Array.from(resultIndices)].sort((a, b) => a - b);
99
+ }
100
+ }
101
+ }
102
+ return [index];
103
+ }
104
+ export function buildBudgetedMessages(messages, options) {
105
+ if (messages.length === 0)
106
+ return messages;
107
+ const normalized = cloneWithTrimmedSystemMessages(messages, options.maxSystemChars);
108
+ const systemCount = getLeadingSystemCount(normalized);
109
+ const leadingSystem = normalized.slice(0, systemCount);
110
+ const history = normalized.slice(systemCount);
111
+ const usableBudget = Math.max(1, options.maxContextTokens - options.reserveOutputTokens);
112
+ let used = leadingSystem.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
113
+ const requiredIndices = new Set();
114
+ for (let i = history.length - 1; i >= 0; i--) {
115
+ if (history[i].role === "user") {
116
+ requiredIndices.add(i);
117
+ break;
118
+ }
119
+ }
120
+ const assistantToToolResults = collectAssistantToolPairings(history);
121
+ const included = new Set();
122
+ const visited = new Set();
123
+ for (let i = history.length - 1; i >= 0; i--) {
124
+ if (visited.has(i))
125
+ continue;
126
+ const group = buildAtomicGroupIndices(i, history, assistantToToolResults);
127
+ for (const idx of group)
128
+ visited.add(idx);
129
+ const groupTokens = group.reduce((sum, idx) => sum + estimateMessageTokens(history[idx]), 0);
130
+ const isRequired = group.some((idx) => requiredIndices.has(idx));
131
+ if (isRequired || used + groupTokens <= usableBudget) {
132
+ for (const idx of group)
133
+ included.add(idx);
134
+ used += groupTokens;
135
+ }
136
+ }
137
+ const selectedHistory = history.filter((_, index) => included.has(index));
138
+ return [...leadingSystem, ...selectedHistory];
139
+ }
@@ -1,6 +1,15 @@
1
- import { streamText } from "ai";
1
+ import { streamText, Output } from "ai";
2
+ async function toInlineDataUrl(url, mimeType) {
3
+ const response = await fetch(url);
4
+ if (!response.ok) {
5
+ throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
6
+ }
7
+ const arrayBuffer = await response.arrayBuffer();
8
+ const base64 = Buffer.from(arrayBuffer).toString("base64");
9
+ return `data:${mimeType};base64,${base64}`;
10
+ }
2
11
  async function toModelMessages(messages) {
3
- return messages.map((message) => {
12
+ return Promise.all(messages.map(async (message) => {
4
13
  if (message.role === "tool") {
5
14
  return {
6
15
  role: "tool",
@@ -50,23 +59,33 @@ async function toModelMessages(messages) {
50
59
  }
51
60
  if (message.role === "user") {
52
61
  if (message.attachments && message.attachments.length > 0) {
62
+ const attachmentParts = await Promise.all(message.attachments.map(async (a) => {
63
+ if (a.mimeType.startsWith("image/")) {
64
+ try {
65
+ return {
66
+ type: "image",
67
+ image: await toInlineDataUrl(a.url, a.mimeType),
68
+ };
69
+ }
70
+ catch (error) {
71
+ console.warn(`Failed to inline image attachment (${a.name}). Falling back to URL: ${a.url}`, error);
72
+ return {
73
+ type: "image",
74
+ image: a.url,
75
+ };
76
+ }
77
+ }
78
+ return {
79
+ type: "file",
80
+ data: a.url,
81
+ mimeType: a.mimeType,
82
+ };
83
+ }));
53
84
  return {
54
85
  role: "user",
55
86
  content: [
56
87
  { type: "text", text: message.content },
57
- ...message.attachments.map((a) => {
58
- if (a.mimeType.startsWith("image/")) {
59
- return {
60
- type: "image",
61
- image: a.url,
62
- };
63
- }
64
- return {
65
- type: "file",
66
- data: a.url,
67
- mimeType: a.mimeType,
68
- };
69
- }),
88
+ ...attachmentParts,
70
89
  ],
71
90
  };
72
91
  }
@@ -79,7 +98,7 @@ async function toModelMessages(messages) {
79
98
  role: message.role,
80
99
  content: message.content,
81
100
  };
82
- });
101
+ }));
83
102
  }
84
103
  // Helper to find pending tool calls in history
85
104
  function getPendingToolCalls(messages) {
@@ -156,7 +175,7 @@ function insertToolResult(messages, toolResultMsg) {
156
175
  * It can also automatically trigger events based on tool calls.
157
176
  */
158
177
  export const llmPlugin = (options) => (builder) => {
159
- const { model, system, toolDefinitions = {}, actionEventPrefix = "action:", promptInputType = "agent:input", actionResultInputType = "action:result", completionEventType = "agent:output", usageEventType = "usage:update", usageScope = "default", modelId, } = options;
178
+ const { model, system, toolDefinitions = {}, actionEventPrefix = "action:", promptInputType = "agent:input", actionResultInputType = "action:result", completionEventType = "agent:output", usageEventType = "usage:update", usageScope = "default", modelId, outputSchema, } = options;
160
179
  async function* routeToLLM(newMessage, context, silent = false) {
161
180
  const state = context.state;
162
181
  if (!state.messages) {
@@ -217,19 +236,40 @@ export const llmPlugin = (options) => (builder) => {
217
236
  system: systemPrompt,
218
237
  messages: modelMessages,
219
238
  tools: toolDefinitions,
239
+ output: outputSchema ? Output.object({ schema: outputSchema }) : undefined,
220
240
  onError: (error) => {
221
241
  console.error("streamText error:::::", JSON.stringify(error, null, 2));
222
242
  },
223
243
  });
224
- for await (const delta of result.textStream) {
225
- assistantMessage.content += delta;
226
- if (!silent) {
244
+ if (outputSchema) {
245
+ for await (const delta of result.partialOutputStream) {
246
+ if (!silent) {
247
+ yield {
248
+ type: "agent:output-delta",
249
+ data: { delta: "", content: JSON.stringify(delta) },
250
+ };
251
+ }
252
+ }
253
+ const finalObject = await result.output;
254
+ assistantMessage.content = JSON.stringify(finalObject);
255
+ if (completionEventType && !silent) {
227
256
  yield {
228
- type: "agent:output-delta",
229
- data: { delta, content: assistantMessage.content },
257
+ type: completionEventType,
258
+ data: finalObject,
230
259
  };
231
260
  }
232
261
  }
262
+ else {
263
+ for await (const delta of result.textStream) {
264
+ assistantMessage.content += delta;
265
+ if (!silent) {
266
+ yield {
267
+ type: "agent:output-delta",
268
+ data: { delta, content: assistantMessage.content },
269
+ };
270
+ }
271
+ }
272
+ }
233
273
  const assistantText = assistantMessage.content;
234
274
  // Wait for tool calls to complete
235
275
  const toolCalls = await result.toolCalls;
@@ -252,10 +292,13 @@ export const llmPlugin = (options) => (builder) => {
252
292
  }
253
293
  else {
254
294
  if (completionEventType && !silent) {
255
- yield {
256
- type: completionEventType,
257
- data: { content: assistantText },
258
- };
295
+ // If it's structured output, we already yielded the final object
296
+ if (!outputSchema) {
297
+ yield {
298
+ type: completionEventType,
299
+ data: { content: assistantText },
300
+ };
301
+ }
259
302
  }
260
303
  }
261
304
  const usage = await result.usage;
@@ -307,20 +350,31 @@ export const llmPlugin = (options) => (builder) => {
307
350
  });
308
351
  // Feed action results back as system-role feedback to the model.
309
352
  builder.on(actionResultInputType, async function* (event, context) {
310
- const { action, result, toolCallId } = event.data;
353
+ const { action, result, toolCallId, halt } = event.data;
311
354
  const normalizedAction = typeof action === "string" ? action : "unknown";
312
355
  const summary = typeof result === "string" ? result : JSON.stringify(result);
313
- yield* routeToLLM({
356
+ const toolResultMessage = {
314
357
  role: "tool",
315
358
  content: [{
316
- type: 'tool-result',
359
+ type: "tool-result",
317
360
  toolCallId,
318
361
  toolName: normalizedAction,
319
362
  output: {
320
- type: 'text',
363
+ type: "text",
321
364
  value: summary,
322
365
  },
323
366
  }],
324
- }, context);
367
+ };
368
+ // Hard-stop mode: record tool result to unblock pending calls, but do not
369
+ // continue the autonomous tool loop in this turn.
370
+ if (halt === true) {
371
+ const state = context.state;
372
+ if (!state.messages) {
373
+ state.messages = [];
374
+ }
375
+ insertToolResult(state.messages, toolResultMessage);
376
+ return;
377
+ }
378
+ yield* routeToLLM(toolResultMessage, context);
325
379
  });
326
380
  };
@@ -1,4 +1,4 @@
1
- import { ui } from "@melony/ui-kit/server";
1
+ import { uiEvent } from "../../ui/block.js";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { createMemoryModule } from "./memory.js";
@@ -214,7 +214,7 @@ export const memoryPlugin = (options) => (builder) => {
214
214
  }
215
215
  });
216
216
  builder.on("memory:status", async function* (event) {
217
- yield ui.event(statusWidget(event.data.message, event.data.severity));
217
+ yield uiEvent(statusWidget(event.data.message, event.data.severity));
218
218
  });
219
219
  };
220
220
  export default memoryPlugin;
@@ -1,4 +1,4 @@
1
- import { ui } from "@melony/ui-kit/server";
1
+ import { uiEvent } from "../../ui/block.js";
2
2
  import { z } from "zod";
3
3
  import { exec } from "node:child_process";
4
4
  import { promisify } from "node:util";
@@ -95,6 +95,6 @@ export const shellPlugin = (options = {}) => (builder) => {
95
95
  }
96
96
  });
97
97
  builder.on("shell:status", async function* (event) {
98
- yield ui.event(statusWidget(event.data.message, event.data.severity));
98
+ yield uiEvent(statusWidget(event.data.message, event.data.severity));
99
99
  });
100
100
  };
@@ -1,4 +1,4 @@
1
- import { ui } from "@melony/ui-kit/server";
1
+ import { uiEvent, block } from "../../ui/block.js";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import matter from "gray-matter";
@@ -275,11 +275,11 @@ export const skillsPlugin = (options) => (builder) => {
275
275
  }
276
276
  });
277
277
  builder.on("skills:status", async function* (event) {
278
- yield ui.event(statusWidget(event.data.message, event.data.severity));
278
+ yield uiEvent(statusWidget(event.data.message, event.data.severity));
279
279
  });
280
280
  builder.on("skills:loaded", async function* (event) {
281
- yield ui.event(resourceCardWidget(event.data.title, "", [
282
- ui.text(event.data.instructions),
281
+ yield uiEvent(resourceCardWidget(event.data.title, "", [
282
+ block('text', { value: event.data.instructions }),
283
283
  ]));
284
284
  });
285
285
  };