macroclaw 0.30.0 → 0.32.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macroclaw",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Telegram-to-Claude-Code bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -130,7 +130,7 @@ const setupCommand = defineCommand({
130
130
  meta: { name: "setup", description: "Run the interactive setup wizard" },
131
131
  args: {
132
132
  "skip-service": { type: "boolean", description: "Skip the service installation prompt" },
133
- "install-service": { type: "boolean", description: "Install as a system service without prompting" },
133
+ "install-service": { type: "boolean", description: "Install as a service without prompting" },
134
134
  },
135
135
  run: ({ args }) => defaultCli.setup({ skipService: args["skip-service"], installService: args["install-service"] }).catch(handleError),
136
136
  });
@@ -141,7 +141,7 @@ const claudeCommand = defineCommand({
141
141
  });
142
142
 
143
143
  const serviceInstallCommand = defineCommand({
144
- meta: { name: "install", description: "Install and start macroclaw as a system service" },
144
+ meta: { name: "install", description: "Install and start macroclaw as a service" },
145
145
  args: {
146
146
  token: { type: "string", description: "Claude OAuth token from `claude setup-token` (required on macOS)" },
147
147
  },
@@ -149,17 +149,17 @@ const serviceInstallCommand = defineCommand({
149
149
  });
150
150
 
151
151
  const serviceUninstallCommand = defineCommand({
152
- meta: { name: "uninstall", description: "Stop and remove the system service" },
152
+ meta: { name: "uninstall", description: "Stop and remove the service" },
153
153
  run: () => { try { defaultCli.service("uninstall"); } catch (err) { handleError(err); } },
154
154
  });
155
155
 
156
156
  const serviceStartCommand = defineCommand({
157
- meta: { name: "start", description: "Start the system service" },
157
+ meta: { name: "start", description: "Start the service" },
158
158
  run: () => { try { defaultCli.service("start"); } catch (err) { handleError(err); } },
159
159
  });
160
160
 
161
161
  const serviceStopCommand = defineCommand({
162
- meta: { name: "stop", description: "Stop the system service" },
162
+ meta: { name: "stop", description: "Stop the service" },
163
163
  run: () => { try { defaultCli.service("stop"); } catch (err) { handleError(err); } },
164
164
  });
165
165
 
@@ -182,7 +182,7 @@ const serviceLogsCommand = defineCommand({
182
182
  });
183
183
 
184
184
  const serviceCommand = defineCommand({
185
- meta: { name: "service", description: "Manage macroclaw system service" },
185
+ meta: { name: "service", description: "Manage macroclaw service" },
186
186
  subCommands: {
187
187
  install: serviceInstallCommand,
188
188
  uninstall: serviceUninstallCommand,
@@ -9,7 +9,17 @@ import {
9
9
  import { writeHistoryPrompt, writeHistoryResult } from "./history";
10
10
  import { createLogger } from "./logger";
11
11
  import { generateName } from "./naming";
12
- import { buildEvent, type EventInput, SYSTEM_PROMPT } from "./prompts";
12
+ import {
13
+ backgroundAgentProgressEvent,
14
+ backgroundAgentResultEvent,
15
+ backgroundAgentStartEvent,
16
+ buttonClickEvent,
17
+ healthCheckEvent,
18
+ peekEvent,
19
+ SYSTEM_PROMPT,
20
+ scheduleTriggerEvent,
21
+ userMessageEvent,
22
+ } from "./prompts";
13
23
  import { Queue } from "./queue";
14
24
  import { loadSessions, saveSessions } from "./sessions";
15
25
 
@@ -136,13 +146,11 @@ export class Orchestrator {
136
146
 
137
147
  handleCron(name: string, prompt: string, model?: string, missed?: { missedBy: string; scheduledAt: string }): void {
138
148
  const cronName = `cron-${name}`;
139
- const formatted = buildEvent({
140
- name: cronName,
141
- type: "schedule-trigger",
142
- session: "background",
143
- schedule: { name, missedBy: missed?.missedBy, scheduledAt: missed?.scheduledAt },
144
- text: prompt,
145
- });
149
+ const formatted = scheduleTriggerEvent(
150
+ cronName,
151
+ { name, missedBy: missed?.missedBy, scheduledAt: missed?.scheduledAt },
152
+ prompt,
153
+ );
146
154
  this.#spawnBackgroundRaw(cronName, prompt, formatted, model ?? this.#config.model);
147
155
  }
148
156
 
@@ -209,13 +217,11 @@ export class Orchestrator {
209
217
  this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(session.name)}</b>...` });
210
218
 
211
219
  try {
212
- const prompt = buildEvent({
213
- name: `peek-${session.name}`,
214
- type: "peek",
215
- session: "background",
216
- targetEvent: session.name,
217
- instructions: `Only consider progress since the "${session.name}" event. Brief status update: done, in progress, remaining. 2-3 sentences max, plain text.`,
218
- });
220
+ const prompt = peekEvent(
221
+ `peek-${session.name}`,
222
+ session.name,
223
+ `Only consider progress since the "${session.name}" event. Brief status update: done, in progress, remaining. 2-3 sentences max, plain text.`,
224
+ );
219
225
  const query = this.#claude.forkSession(
220
226
  sessionId,
221
227
  prompt,
@@ -372,56 +378,28 @@ export class Orchestrator {
372
378
  }
373
379
 
374
380
  #formatPrompt(request: OrchestratorRequest, name: string, backgroundedEvent?: string): string {
375
- let input: EventInput;
376
-
377
381
  switch (request.type) {
378
382
  case "user":
379
- input = {
380
- name,
381
- type: "user-message",
382
- session: "main",
383
- text: request.message || undefined,
384
- files: request.files,
385
- backgroundedEvent,
386
- };
387
- break;
383
+ return userMessageEvent(name, request.message || "", { files: request.files, backgroundedEvent });
388
384
  case "background-agent-result":
389
- input = {
385
+ return backgroundAgentResultEvent(
390
386
  name,
391
- type: "background-agent-result",
392
- session: "main",
393
- originalEvent: request.name,
394
- result: {
395
- text: request.response.message || "[No output]",
396
- files: request.response.files,
397
- },
398
- backgroundedEvent,
399
- instructions: "Forward this result to the user (action=\"send\"). Summarize or add context from the conversation as appropriate.",
400
- };
401
- break;
387
+ request.name,
388
+ { text: request.response.message || "[No output]", files: request.response.files },
389
+ "Forward this result to the user (action=\"send\"). Summarize or add context from the conversation as appropriate.",
390
+ { backgroundedEvent },
391
+ );
402
392
  case "background-agent-progress":
403
- input = {
393
+ return backgroundAgentProgressEvent(
404
394
  name,
405
- type: "background-agent-progress",
406
- session: "main",
407
- originalEvent: request.name,
408
- progress: request.progress,
409
- instructions: "This is an interim progress update, not a final result. Do not report to the user unless it contains exceptionally important information.",
410
- backgroundedEvent,
411
- };
412
- break;
395
+ request.name,
396
+ request.progress,
397
+ "This is an interim progress update, not a final result. Do not report to the user unless it contains exceptionally important information.",
398
+ { backgroundedEvent },
399
+ );
413
400
  case "button":
414
- input = {
415
- name,
416
- type: "button-click",
417
- session: "main",
418
- button: request.label,
419
- backgroundedEvent,
420
- };
421
- break;
401
+ return buttonClickEvent(name, request.label, { backgroundedEvent });
422
402
  }
423
-
424
- return buildEvent(input);
425
403
  }
426
404
 
427
405
  static #requestLabel(request: OrchestratorRequest): string {
@@ -455,12 +433,7 @@ export class Orchestrator {
455
433
  // --- Background management ---
456
434
 
457
435
  #spawnBackground(name: string, prompt: string, model: string | undefined) {
458
- const formatted = buildEvent({
459
- name,
460
- type: "background-agent-start",
461
- session: "background",
462
- text: prompt,
463
- });
436
+ const formatted = backgroundAgentStartEvent(name, prompt);
464
437
  this.#spawnBackgroundRaw(name, prompt, formatted, model);
465
438
  }
466
439
 
@@ -523,13 +496,11 @@ export class Orchestrator {
523
496
 
524
497
  log.debug({ name: info.name, sessionId }, "Running health check");
525
498
 
526
- const prompt = buildEvent({
527
- name: `health-check-${info.name}`,
528
- type: "health-check",
529
- session: "background",
530
- targetEvent: info.name,
531
- instructions: "Report your current status. If your task is complete, set finished=true and provide the full output. If still working, set finished=false and describe current progress in one sentence.",
532
- });
499
+ const prompt = healthCheckEvent(
500
+ `health-check-${info.name}`,
501
+ info.name,
502
+ "Report your current status. If your task is complete, set finished=true and provide the full output. If still working, set finished=false and describe current progress in one sentence.",
503
+ );
533
504
 
534
505
  let query: RunningQuery<z.infer<typeof healthCheckSchema>>;
535
506
  try {
@@ -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
  });