macroclaw 0.31.0 → 0.33.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,5 +1,15 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { buildEvent, escapeXml, SYSTEM_PROMPT } from "./prompts";
2
+ import {
3
+ backgroundAgentProgressEvent,
4
+ backgroundAgentResultEvent,
5
+ backgroundAgentStartEvent,
6
+ buttonClickEvent,
7
+ healthCheckEvent,
8
+ peekEvent,
9
+ SYSTEM_PROMPT,
10
+ scheduleTriggerEvent,
11
+ userMessageEvent,
12
+ } from "./prompts";
3
13
 
4
14
  describe("SYSTEM_PROMPT", () => {
5
15
  it("contains key sections", () => {
@@ -50,35 +60,16 @@ describe("SYSTEM_PROMPT", () => {
50
60
  });
51
61
  });
52
62
 
53
- describe("escapeXml", () => {
54
- it("escapes &, <, >, \"", () => {
55
- expect(escapeXml('a & b < c > d "e"')).toBe("a &amp; b &lt; c &gt; d &quot;e&quot;");
56
- });
57
-
58
- it("returns plain text unchanged", () => {
59
- expect(escapeXml("hello world")).toBe("hello world");
60
- });
61
- });
62
-
63
- describe("buildEvent", () => {
63
+ describe("userMessageEvent", () => {
64
64
  it("builds user message event", () => {
65
- const result = buildEvent({
66
- name: "check-logs",
67
- type: "user-message",
68
- session: "main",
69
- text: "hello",
70
- });
65
+ const result = userMessageEvent("check-logs", "hello");
71
66
  expect(result).toStartWith('<event name="check-logs" type="user-message" session="main">');
72
67
  expect(result).toContain("<text>hello</text>");
73
68
  expect(result).toEndWith("</event>");
74
69
  });
75
70
 
76
71
  it("builds user message with files", () => {
77
- const result = buildEvent({
78
- name: "analyze-photo",
79
- type: "user-message",
80
- session: "main",
81
- text: "what's in this image?",
72
+ const result = userMessageEvent("analyze-photo", "what's in this image?", {
82
73
  files: ["/tmp/photo.jpg", "/tmp/doc.pdf"],
83
74
  });
84
75
  expect(result).toContain("<text>what's in this image?</text>");
@@ -88,74 +79,66 @@ describe("buildEvent", () => {
88
79
  expect(result).toContain("</files>");
89
80
  });
90
81
 
91
- it("builds user message with files only (no text)", () => {
92
- const result = buildEvent({
93
- name: "task",
94
- type: "user-message",
95
- session: "main",
96
- files: ["/tmp/photo.jpg"],
97
- });
98
- expect(result).not.toContain("<text>");
99
- expect(result).toContain('<file path="/tmp/photo.jpg" />');
100
- });
101
-
102
82
  it("builds user message with backgrounded event", () => {
103
- const result = buildEvent({
104
- name: "check-logs",
105
- type: "user-message",
106
- session: "main",
83
+ const result = userMessageEvent("check-logs", "check the logs", {
107
84
  backgroundedEvent: "deploy-cluster",
108
- text: "check the logs",
109
85
  });
110
86
  expect(result).toContain('<backgrounded-event name="deploy-cluster" />');
111
87
  expect(result).toContain("<text>check the logs</text>");
112
88
  });
113
89
 
114
90
  it("places backgrounded-event before text", () => {
115
- const result = buildEvent({
116
- name: "check-logs",
117
- type: "user-message",
118
- session: "main",
91
+ const result = userMessageEvent("check-logs", "hello", {
119
92
  backgroundedEvent: "deploy",
120
- text: "hello",
121
93
  });
122
94
  const bgIdx = result.indexOf("backgrounded-event");
123
95
  const textIdx = result.indexOf("<text>");
124
96
  expect(bgIdx).toBeLessThan(textIdx);
125
97
  });
126
98
 
127
- it("builds button click event", () => {
128
- const result = buildEvent({
129
- name: "btn-yes",
130
- type: "button-click",
131
- session: "main",
132
- button: "Yes",
99
+ it("escapes XML in text content", () => {
100
+ const result = userMessageEvent("test", "a < b & c > d");
101
+ expect(result).toContain("<text>a &lt; b &amp; c &gt; d</text>");
102
+ });
103
+
104
+ it("escapes XML in name attribute", () => {
105
+ const result = userMessageEvent('a & "b"', "test");
106
+ expect(result).toContain('name="a &amp; &quot;b&quot;"');
107
+ });
108
+
109
+ it("escapes XML in backgrounded event name", () => {
110
+ const result = userMessageEvent("test", "hello", {
111
+ backgroundedEvent: 'task & "stuff"',
133
112
  });
113
+ expect(result).toContain('backgrounded-event name="task &amp; &quot;stuff&quot;"');
114
+ });
115
+ });
116
+
117
+ describe("buttonClickEvent", () => {
118
+ it("builds button click event", () => {
119
+ const result = buttonClickEvent("btn-yes", "Yes");
134
120
  expect(result).toContain('type="button-click"');
135
121
  expect(result).toContain("<button>Yes</button>");
136
122
  expect(result).not.toContain("<text>");
137
123
  });
138
124
 
139
125
  it("builds button click with backgrounded event", () => {
140
- const result = buildEvent({
141
- name: "btn-yes",
142
- type: "button-click",
143
- session: "main",
144
- button: "Yes",
126
+ const result = buttonClickEvent("btn-yes", "Yes", {
145
127
  backgroundedEvent: "deploy-cluster",
146
128
  });
147
129
  expect(result).toContain('<backgrounded-event name="deploy-cluster" />');
148
130
  expect(result).toContain("<button>Yes</button>");
149
131
  });
150
132
 
133
+ it("escapes XML in button label", () => {
134
+ const result = buttonClickEvent("btn", 'a & "b"');
135
+ expect(result).toContain("<button>a &amp; &quot;b&quot;</button>");
136
+ });
137
+ });
138
+
139
+ describe("scheduleTriggerEvent", () => {
151
140
  it("builds schedule trigger event", () => {
152
- const result = buildEvent({
153
- name: "cron-daily",
154
- type: "schedule-trigger",
155
- session: "background",
156
- schedule: { name: "daily" },
157
- text: "check updates",
158
- });
141
+ const result = scheduleTriggerEvent("cron-daily", { name: "daily" }, "check updates");
159
142
  expect(result).toContain('type="schedule-trigger"');
160
143
  expect(result).toContain('session="background"');
161
144
  expect(result).toContain('<schedule name="daily" />');
@@ -163,38 +146,34 @@ describe("buildEvent", () => {
163
146
  });
164
147
 
165
148
  it("builds missed schedule trigger with attributes", () => {
166
- const result = buildEvent({
167
- name: "cron-reminder",
168
- type: "schedule-trigger",
169
- session: "background",
170
- schedule: { name: "reminder", missedBy: "15m", scheduledAt: "2026-03-20T06:00:00Z" },
171
- text: "buy milk",
172
- });
149
+ const result = scheduleTriggerEvent(
150
+ "cron-reminder",
151
+ { name: "reminder", missedBy: "15m", scheduledAt: "2026-03-20T06:00:00Z" },
152
+ "buy milk",
153
+ );
173
154
  expect(result).toContain('missed-by="15m"');
174
155
  expect(result).toContain('scheduled-at="2026-03-20T06:00:00Z"');
175
156
  expect(result).toContain("<text>buy milk</text>");
176
157
  });
158
+ });
177
159
 
160
+ describe("backgroundAgentStartEvent", () => {
178
161
  it("builds background agent start event", () => {
179
- const result = buildEvent({
180
- name: "research",
181
- type: "background-agent-start",
182
- session: "background",
183
- text: "find papers about transformers",
184
- });
162
+ const result = backgroundAgentStartEvent("research", "find papers about transformers");
185
163
  expect(result).toContain('type="background-agent-start"');
186
164
  expect(result).toContain('session="background"');
187
165
  expect(result).toContain("<text>find papers about transformers</text>");
188
166
  });
167
+ });
189
168
 
169
+ describe("backgroundAgentResultEvent", () => {
190
170
  it("builds background agent result (text only)", () => {
191
- const result = buildEvent({
192
- name: "bg-research",
193
- type: "background-agent-result",
194
- session: "main",
195
- originalEvent: "research",
196
- result: { text: "found 3 papers" },
197
- });
171
+ const result = backgroundAgentResultEvent(
172
+ "bg-research",
173
+ "research",
174
+ { text: "found 3 papers" },
175
+ "Forward to user.",
176
+ );
198
177
  expect(result).toContain('type="background-agent-result"');
199
178
  expect(result).toContain('<original-event name="research" />');
200
179
  expect(result).toContain("<result>");
@@ -204,102 +183,63 @@ describe("buildEvent", () => {
204
183
  });
205
184
 
206
185
  it("builds background agent result with files", () => {
207
- const result = buildEvent({
208
- name: "bg-research",
209
- type: "background-agent-result",
210
- session: "main",
211
- originalEvent: "research",
212
- result: { text: "here are the screenshots", files: ["/tmp/screenshot.png"] },
213
- });
186
+ const result = backgroundAgentResultEvent(
187
+ "bg-research",
188
+ "research",
189
+ { text: "here are the screenshots", files: ["/tmp/screenshot.png"] },
190
+ "Forward to user.",
191
+ );
214
192
  expect(result).toContain("<result>");
215
193
  expect(result).toContain("<text>here are the screenshots</text>");
216
194
  expect(result).toContain('<file path="/tmp/screenshot.png" />');
217
195
  expect(result).toContain("</result>");
218
196
  });
219
197
 
220
- it("builds peek event with instructions", () => {
221
- const result = buildEvent({
222
- name: "peek-deploy",
223
- type: "peek",
224
- session: "background",
225
- targetEvent: "deploy",
226
- instructions: "Brief status update.",
227
- });
228
- expect(result).toContain('type="peek"');
229
- expect(result).toContain('<target-event name="deploy" />');
230
- expect(result).toContain("<instructions>Brief status update.</instructions>");
231
- expect(result).not.toContain("<text>");
232
- });
233
-
234
- it("builds progress event with progress tag", () => {
235
- const result = buildEvent({
236
- name: "progress-research",
237
- type: "background-agent-progress",
238
- session: "main",
239
- originalEvent: "research",
240
- progress: "indexing 500 documents",
241
- });
242
- expect(result).toContain('type="background-agent-progress"');
243
- expect(result).toContain('<original-event name="research" />');
244
- expect(result).toContain("<progress>indexing 500 documents</progress>");
245
- expect(result).not.toContain("<result>");
246
- });
247
-
248
- it("includes instructions in event", () => {
249
- const result = buildEvent({
250
- name: "bg-research",
251
- type: "background-agent-result",
252
- session: "main",
253
- originalEvent: "research",
254
- result: { text: "done" },
255
- instructions: "Forward to user.",
256
- });
198
+ it("includes instructions after result", () => {
199
+ const result = backgroundAgentResultEvent(
200
+ "bg-research",
201
+ "research",
202
+ { text: "done" },
203
+ "Forward to user.",
204
+ );
257
205
  expect(result).toContain("<instructions>Forward to user.</instructions>");
258
- // instructions come last, before </event>
259
206
  const instrIdx = result.indexOf("<instructions>");
260
207
  const closeIdx = result.indexOf("</event>");
261
208
  expect(instrIdx).toBeLessThan(closeIdx);
262
209
  expect(instrIdx).toBeGreaterThan(result.indexOf("</result>"));
263
210
  });
211
+ });
264
212
 
265
- it("escapes XML in text content", () => {
266
- const result = buildEvent({
267
- name: "test",
268
- type: "user-message",
269
- session: "main",
270
- text: "a < b & c > d",
271
- });
272
- expect(result).toContain("<text>a &lt; b &amp; c &gt; d</text>");
273
- });
274
-
275
- it("escapes XML in name attribute", () => {
276
- const result = buildEvent({
277
- name: 'a & "b"',
278
- type: "user-message",
279
- session: "main",
280
- text: "test",
281
- });
282
- expect(result).toContain('name="a &amp; &quot;b&quot;"');
213
+ describe("backgroundAgentProgressEvent", () => {
214
+ it("builds progress event with progress tag", () => {
215
+ const result = backgroundAgentProgressEvent(
216
+ "progress-research",
217
+ "research",
218
+ "indexing 500 documents",
219
+ "Do not report unless important.",
220
+ );
221
+ expect(result).toContain('type="background-agent-progress"');
222
+ expect(result).toContain('<original-event name="research" />');
223
+ expect(result).toContain("<progress>indexing 500 documents</progress>");
224
+ expect(result).not.toContain("<result>");
283
225
  });
226
+ });
284
227
 
285
- it("escapes XML in button label", () => {
286
- const result = buildEvent({
287
- name: "btn",
288
- type: "button-click",
289
- session: "main",
290
- button: 'a & "b"',
291
- });
292
- expect(result).toContain("<button>a &amp; &quot;b&quot;</button>");
228
+ describe("peekEvent", () => {
229
+ it("builds peek event with instructions", () => {
230
+ const result = peekEvent("peek-deploy", "deploy", "Brief status update.");
231
+ expect(result).toContain('type="peek"');
232
+ expect(result).toContain('<target-event name="deploy" />');
233
+ expect(result).toContain("<instructions>Brief status update.</instructions>");
234
+ expect(result).not.toContain("<text>");
293
235
  });
236
+ });
294
237
 
295
- it("escapes XML in backgrounded event name", () => {
296
- const result = buildEvent({
297
- name: "test",
298
- type: "user-message",
299
- session: "main",
300
- backgroundedEvent: 'task & "stuff"',
301
- text: "hello",
302
- });
303
- expect(result).toContain('backgrounded-event name="task &amp; &quot;stuff&quot;"');
238
+ describe("healthCheckEvent", () => {
239
+ it("builds health check event with instructions", () => {
240
+ const result = healthCheckEvent("health-check-deploy", "deploy", "Report status.");
241
+ expect(result).toContain('type="health-check"');
242
+ expect(result).toContain('<target-event name="deploy" />');
243
+ expect(result).toContain("<instructions>Report status.</instructions>");
304
244
  });
305
245
  });
package/src/prompts.ts CHANGED
@@ -67,26 +67,11 @@ Each button gets its own row. Max 27 characters per label — if options need mo
67
67
 
68
68
  // --- Event builder ---
69
69
 
70
- export function escapeXml(text: string): string {
70
+ function escapeXml(text: string): string {
71
71
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
72
72
  }
73
73
 
74
- export type SessionType = "main" | "background";
75
-
76
- export type EventType =
77
- | "user-message"
78
- | "button-click"
79
- | "schedule-trigger"
80
- | "background-agent-start"
81
- | "background-agent-result"
82
- | "background-agent-progress"
83
- | "peek"
84
- | "health-check";
85
-
86
- export interface EventInput {
87
- name: string;
88
- type: EventType;
89
- session: SessionType;
74
+ interface BuildXmlFields {
90
75
  text?: string;
91
76
  files?: string[];
92
77
  button?: string;
@@ -99,46 +84,40 @@ export interface EventInput {
99
84
  result?: { text: string; files?: string[] };
100
85
  }
101
86
 
102
- export function buildEvent(input: EventInput): string {
87
+ function buildXml(name: string, type: string, session: string, fields: BuildXmlFields): string {
103
88
  const lines: string[] = [
104
- `<event name="${escapeXml(input.name)}" type="${input.type}" session="${input.session}">`,
89
+ `<event name="${escapeXml(name)}" type="${type}" session="${session}">`,
105
90
  ];
106
91
 
107
- // Backgrounded event (always before content for visibility)
108
- if (input.backgroundedEvent) {
109
- lines.push(`<backgrounded-event name="${escapeXml(input.backgroundedEvent)}" />`);
92
+ if (fields.backgroundedEvent) {
93
+ lines.push(`<backgrounded-event name="${escapeXml(fields.backgroundedEvent)}" />`);
110
94
  }
111
95
 
112
- // Schedule metadata
113
- if (input.schedule) {
114
- const attrs = [`name="${escapeXml(input.schedule.name)}"`];
115
- if (input.schedule.missedBy) attrs.push(`missed-by="${escapeXml(input.schedule.missedBy)}"`);
116
- if (input.schedule.scheduledAt) attrs.push(`scheduled-at="${escapeXml(input.schedule.scheduledAt)}"`);
96
+ if (fields.schedule) {
97
+ const attrs = [`name="${escapeXml(fields.schedule.name)}"`];
98
+ if (fields.schedule.missedBy) attrs.push(`missed-by="${escapeXml(fields.schedule.missedBy)}"`);
99
+ if (fields.schedule.scheduledAt) attrs.push(`scheduled-at="${escapeXml(fields.schedule.scheduledAt)}"`);
117
100
  lines.push(`<schedule ${attrs.join(" ")} />`);
118
101
  }
119
102
 
120
- // Original event reference (for background-agent-result)
121
- if (input.originalEvent) {
122
- lines.push(`<original-event name="${escapeXml(input.originalEvent)}" />`);
103
+ if (fields.originalEvent) {
104
+ lines.push(`<original-event name="${escapeXml(fields.originalEvent)}" />`);
123
105
  }
124
106
 
125
- // Target event reference (for peek)
126
- if (input.targetEvent) {
127
- lines.push(`<target-event name="${escapeXml(input.targetEvent)}" />`);
107
+ if (fields.targetEvent) {
108
+ lines.push(`<target-event name="${escapeXml(fields.targetEvent)}" />`);
128
109
  }
129
110
 
130
- // Progress (for background-agent-progress)
131
- if (input.progress) {
132
- lines.push(`<progress>${escapeXml(input.progress)}</progress>`);
111
+ if (fields.progress) {
112
+ lines.push(`<progress>${escapeXml(fields.progress)}</progress>`);
133
113
  }
134
114
 
135
- // Result block (for background-agent-result)
136
- if (input.result) {
115
+ if (fields.result) {
137
116
  lines.push("<result>");
138
- lines.push(`<text>${escapeXml(input.result.text)}</text>`);
139
- if (input.result.files?.length) {
117
+ lines.push(`<text>${escapeXml(fields.result.text)}</text>`);
118
+ if (fields.result.files?.length) {
140
119
  lines.push("<files>");
141
- for (const f of input.result.files) {
120
+ for (const f of fields.result.files) {
142
121
  lines.push(` <file path="${escapeXml(f)}" />`);
143
122
  }
144
123
  lines.push("</files>");
@@ -146,30 +125,60 @@ export function buildEvent(input: EventInput): string {
146
125
  lines.push("</result>");
147
126
  }
148
127
 
149
- // Button label
150
- if (input.button) {
151
- lines.push(`<button>${escapeXml(input.button)}</button>`);
128
+ if (fields.button) {
129
+ lines.push(`<button>${escapeXml(fields.button)}</button>`);
152
130
  }
153
131
 
154
- // Text content
155
- if (input.text) {
156
- lines.push(`<text>${escapeXml(input.text)}</text>`);
132
+ if (fields.text) {
133
+ lines.push(`<text>${escapeXml(fields.text)}</text>`);
157
134
  }
158
135
 
159
- // Files
160
- if (input.files?.length) {
136
+ if (fields.files?.length) {
161
137
  lines.push("<files>");
162
- for (const f of input.files) {
138
+ for (const f of fields.files) {
163
139
  lines.push(` <file path="${escapeXml(f)}" />`);
164
140
  }
165
141
  lines.push("</files>");
166
142
  }
167
143
 
168
- // Instructions (inline guidance for long sessions)
169
- if (input.instructions) {
170
- lines.push(`<instructions>${escapeXml(input.instructions)}</instructions>`);
144
+ if (fields.instructions) {
145
+ lines.push(`<instructions>${escapeXml(fields.instructions)}</instructions>`);
171
146
  }
172
147
 
173
148
  lines.push("</event>");
174
149
  return lines.join("\n");
175
150
  }
151
+
152
+ // --- Per-type event builders ---
153
+
154
+ export function userMessageEvent(name: string, text: string, opts?: { files?: string[]; backgroundedEvent?: string }): string {
155
+ return buildXml(name, "user-message", "main", { text, files: opts?.files, backgroundedEvent: opts?.backgroundedEvent });
156
+ }
157
+
158
+ export function buttonClickEvent(name: string, button: string, opts?: { backgroundedEvent?: string }): string {
159
+ return buildXml(name, "button-click", "main", { button, backgroundedEvent: opts?.backgroundedEvent });
160
+ }
161
+
162
+ export function scheduleTriggerEvent(name: string, schedule: { name: string; missedBy?: string; scheduledAt?: string }, text: string): string {
163
+ return buildXml(name, "schedule-trigger", "background", { schedule, text });
164
+ }
165
+
166
+ export function backgroundAgentStartEvent(name: string, text: string): string {
167
+ return buildXml(name, "background-agent-start", "background", { text });
168
+ }
169
+
170
+ export function backgroundAgentResultEvent(name: string, originalEvent: string, result: { text: string; files?: string[] }, instructions: string, opts?: { backgroundedEvent?: string }): string {
171
+ return buildXml(name, "background-agent-result", "main", { originalEvent, result, instructions, backgroundedEvent: opts?.backgroundedEvent });
172
+ }
173
+
174
+ export function backgroundAgentProgressEvent(name: string, originalEvent: string, progress: string, instructions: string, opts?: { backgroundedEvent?: string }): string {
175
+ return buildXml(name, "background-agent-progress", "main", { originalEvent, progress, instructions, backgroundedEvent: opts?.backgroundedEvent });
176
+ }
177
+
178
+ export function peekEvent(name: string, targetEvent: string, instructions: string): string {
179
+ return buildXml(name, "peek", "background", { targetEvent, instructions });
180
+ }
181
+
182
+ export function healthCheckEvent(name: string, targetEvent: string, instructions: string): string {
183
+ return buildXml(name, "health-check", "background", { targetEvent, instructions });
184
+ }