macroclaw 0.34.0 → 0.36.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 +3 -1
- package/src/app.test.ts +2 -0
- package/src/app.ts +4 -1
- package/src/claude.test.ts +8 -0
- package/src/claude.ts +5 -1
- package/src/cli.test.ts +8 -0
- package/src/cli.ts +11 -0
- package/src/index.ts +1 -0
- package/src/orchestrator.test.ts +5 -1
- package/src/orchestrator.ts +15 -21
- package/src/{prompts.test.ts → prompt-builder.test.ts} +67 -68
- package/src/{prompts.ts → prompt-builder.ts} +93 -77
- package/src/scheduler.test.ts +60 -24
- package/src/scheduler.ts +17 -2
- package/src/settings.test.ts +82 -1
- package/src/settings.ts +19 -10
- package/src/setup.test.ts +25 -7
- package/src/setup.ts +9 -1
- package/src/system-service.test.ts +44 -0
- package/src/system-service.ts +10 -0
- package/workspace-template/.claude/skills/schedule/SKILL.md +17 -15
- package/workspace-template/.claude/skills/settings/SKILL.md +54 -0
- package/workspace-template/.claude/skills/settings/scripts/restart.sh +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macroclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Telegram-to-Claude-Code bridge",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"citty": "^0.2.1",
|
|
33
33
|
"cron-parser": "^5.5.0",
|
|
34
34
|
"grammy": "^1.39.3",
|
|
35
|
+
"luxon": "^3.7.2",
|
|
35
36
|
"openai": "^6.27.0",
|
|
36
37
|
"pino": "^10.3.1",
|
|
37
38
|
"pino-pretty": "^13.1.3",
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@biomejs/biome": "^2.4.6",
|
|
45
|
+
"@types/luxon": "^3.7.1",
|
|
44
46
|
"bun-types": "^1.3.10",
|
|
45
47
|
"dependency-cruiser": "^17.3.8",
|
|
46
48
|
"typescript": "^5.9.3"
|
package/src/app.test.ts
CHANGED
|
@@ -132,6 +132,8 @@ function makeConfig(overrides?: Partial<AppConfig>): AppConfig {
|
|
|
132
132
|
botToken: "test-token",
|
|
133
133
|
authorizedChatId: "12345",
|
|
134
134
|
workspace: "/tmp/macroclaw-test-workspace",
|
|
135
|
+
model: "sonnet",
|
|
136
|
+
timezone: "UTC",
|
|
135
137
|
settingsDir: tmpSettingsDir,
|
|
136
138
|
claude: defaultMockClaude(),
|
|
137
139
|
stt: mockStt(),
|
package/src/app.ts
CHANGED
|
@@ -11,7 +11,8 @@ export interface AppConfig {
|
|
|
11
11
|
botToken: string;
|
|
12
12
|
authorizedChatId: string;
|
|
13
13
|
workspace: string;
|
|
14
|
-
model
|
|
14
|
+
model: string;
|
|
15
|
+
timezone: string;
|
|
15
16
|
settingsDir?: string;
|
|
16
17
|
claude?: Claude;
|
|
17
18
|
stt?: SpeechToText;
|
|
@@ -29,6 +30,7 @@ export class App {
|
|
|
29
30
|
this.#orchestrator = new Orchestrator({
|
|
30
31
|
model: config.model,
|
|
31
32
|
workspace: config.workspace,
|
|
33
|
+
timezone: config.timezone,
|
|
32
34
|
settingsDir: config.settingsDir,
|
|
33
35
|
claude: config.claude,
|
|
34
36
|
healthCheckInterval: config.healthCheckInterval,
|
|
@@ -49,6 +51,7 @@ export class App {
|
|
|
49
51
|
start() {
|
|
50
52
|
log.info("Starting macroclaw...");
|
|
51
53
|
const scheduler = new Scheduler(this.#config.workspace, {
|
|
54
|
+
timezone: this.#config.timezone,
|
|
52
55
|
onJob: (name, prompt, model, missed) => this.#orchestrator.handleCron(name, prompt, model, missed),
|
|
53
56
|
});
|
|
54
57
|
scheduler.start();
|
package/src/claude.test.ts
CHANGED
|
@@ -491,5 +491,13 @@ describe("Claude factory", () => {
|
|
|
491
491
|
expect(opts.stdout).toBe("pipe");
|
|
492
492
|
expect(opts.stderr).toBe("pipe");
|
|
493
493
|
});
|
|
494
|
+
|
|
495
|
+
it("merges envVars into the process environment", () => {
|
|
496
|
+
mockSpawn();
|
|
497
|
+
const claude = new Claude({ workspace: TEST_WORKSPACE, envVars: { TZ: "Europe/Prague" } });
|
|
498
|
+
claude.newSession(textResult);
|
|
499
|
+
const opts = spawnOpts();
|
|
500
|
+
expect((opts.env as Record<string, string>).TZ).toBe("Europe/Prague");
|
|
501
|
+
});
|
|
494
502
|
});
|
|
495
503
|
});
|
package/src/claude.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface ClaudeConfig {
|
|
|
18
18
|
workspace: string;
|
|
19
19
|
model?: string;
|
|
20
20
|
systemPrompt?: string;
|
|
21
|
+
/** Extra environment variables to set when spawning the Claude process */
|
|
22
|
+
envVars?: Record<string, string>;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/** Per-call overrides */
|
|
@@ -221,11 +223,13 @@ export class Claude {
|
|
|
221
223
|
readonly #workspace: string;
|
|
222
224
|
readonly #model?: string;
|
|
223
225
|
readonly #systemPrompt?: string;
|
|
226
|
+
readonly #envVars: Record<string, string>;
|
|
224
227
|
|
|
225
228
|
constructor(config: ClaudeConfig) {
|
|
226
229
|
this.#workspace = config.workspace;
|
|
227
230
|
this.#model = config.model;
|
|
228
231
|
this.#systemPrompt = config.systemPrompt;
|
|
232
|
+
this.#envVars = config.envVars ?? {};
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
newSession<R extends ResultType>(resultType: R, options?: QueryOptions): ClaudeProcess<InferResult<R>> {
|
|
@@ -241,7 +245,7 @@ export class Claude {
|
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
#spawn<R extends ResultType>(mode: SessionMode, resultType: R, options?: QueryOptions): ClaudeProcess<InferResult<R>> {
|
|
244
|
-
const env = { ...process.env };
|
|
248
|
+
const env = { ...process.env, ...this.#envVars };
|
|
245
249
|
delete env.CLAUDECODE;
|
|
246
250
|
|
|
247
251
|
const model = options?.model ?? this.#model;
|
package/src/cli.test.ts
CHANGED
|
@@ -133,6 +133,7 @@ function mockService(overrides?: Record<string, unknown>): SystemServiceManager
|
|
|
133
133
|
uninstall: mock(() => {}),
|
|
134
134
|
start: mock(() => ""),
|
|
135
135
|
stop: mock(() => {}),
|
|
136
|
+
restart: mock(() => ""),
|
|
136
137
|
update: mock(() => ({ previousVersion: "0.6.0", currentVersion: "0.7.0" })),
|
|
137
138
|
isRunning: false,
|
|
138
139
|
status: mock(() => ({ installed: false, running: false, platform: "systemd" as const })),
|
|
@@ -170,6 +171,13 @@ describe("Cli.service", () => {
|
|
|
170
171
|
expect(stop).toHaveBeenCalled();
|
|
171
172
|
});
|
|
172
173
|
|
|
174
|
+
it("runs restart action", () => {
|
|
175
|
+
const restart = mock(() => "tail -f /logs");
|
|
176
|
+
const cli = new Cli({ systemService: mockService({ restart }) });
|
|
177
|
+
cli.service("restart");
|
|
178
|
+
expect(restart).toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
173
181
|
it("runs update action — stops and starts when running", () => {
|
|
174
182
|
const stop = mock(() => {});
|
|
175
183
|
const start = mock(() => "tail -f /logs");
|
package/src/cli.ts
CHANGED
|
@@ -75,6 +75,11 @@ export class Cli {
|
|
|
75
75
|
console.log(`Service started. Check logs:\n ${logCmd}`);
|
|
76
76
|
break;
|
|
77
77
|
}
|
|
78
|
+
case "restart": {
|
|
79
|
+
const logCmd = this.#serviceManager.restart();
|
|
80
|
+
console.log(`Service restarted. Check logs:\n ${logCmd}`);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
78
83
|
case "status": {
|
|
79
84
|
const s = this.#serviceManager.status();
|
|
80
85
|
const lines = [
|
|
@@ -173,6 +178,11 @@ const serviceStatusCommand = defineCommand({
|
|
|
173
178
|
run: () => { try { defaultCli.service("status"); } catch (err) { handleError(err); } },
|
|
174
179
|
});
|
|
175
180
|
|
|
181
|
+
const serviceRestartCommand = defineCommand({
|
|
182
|
+
meta: { name: "restart", description: "Restart the service" },
|
|
183
|
+
run: () => { try { defaultCli.service("restart"); } catch (err) { handleError(err); } },
|
|
184
|
+
});
|
|
185
|
+
|
|
176
186
|
const serviceLogsCommand = defineCommand({
|
|
177
187
|
meta: { name: "logs", description: "Print the command to view service logs" },
|
|
178
188
|
args: {
|
|
@@ -188,6 +198,7 @@ const serviceCommand = defineCommand({
|
|
|
188
198
|
uninstall: serviceUninstallCommand,
|
|
189
199
|
start: serviceStartCommand,
|
|
190
200
|
stop: serviceStopCommand,
|
|
201
|
+
restart: serviceRestartCommand,
|
|
191
202
|
update: serviceUpdateCommand,
|
|
192
203
|
status: serviceStatusCommand,
|
|
193
204
|
logs: serviceLogsCommand,
|
package/src/index.ts
CHANGED
package/src/orchestrator.test.ts
CHANGED
|
@@ -116,6 +116,8 @@ function makeOrchestrator(claude: Claude, extraConfig?: Partial<OrchestratorConf
|
|
|
116
116
|
const onResponse = mock(async (r: OrchestratorResponse) => { responses.push(r); });
|
|
117
117
|
const orch = new Orchestrator({
|
|
118
118
|
workspace: TEST_WORKSPACE,
|
|
119
|
+
model: "sonnet",
|
|
120
|
+
timezone: "UTC",
|
|
119
121
|
settingsDir: tmpSettingsDir,
|
|
120
122
|
onResponse,
|
|
121
123
|
claude,
|
|
@@ -737,7 +739,7 @@ describe("Orchestrator", () => {
|
|
|
737
739
|
const detailResponse = responses[responses.length - 1];
|
|
738
740
|
expect(detailResponse.message).toContain("research-pricing");
|
|
739
741
|
expect(detailResponse.message).toContain("research pricing");
|
|
740
|
-
expect(detailResponse.message).toContain("
|
|
742
|
+
expect(detailResponse.message).toContain("sonnet");
|
|
741
743
|
expect(detailResponse.message).toContain("Status: running");
|
|
742
744
|
expect(detailResponse.buttons).toHaveLength(3);
|
|
743
745
|
expect(detailResponse.buttons![0]).toEqual({ text: "Peek", data: `peek:${sessionId}` });
|
|
@@ -1153,6 +1155,8 @@ describe("Orchestrator", () => {
|
|
|
1153
1155
|
const failingOnResponse = mock(async (_r: OrchestratorResponse) => { throw new Error("send failed"); });
|
|
1154
1156
|
const orch = new Orchestrator({
|
|
1155
1157
|
workspace: TEST_WORKSPACE,
|
|
1158
|
+
model: "sonnet",
|
|
1159
|
+
timezone: "UTC",
|
|
1156
1160
|
settingsDir: tmpSettingsDir,
|
|
1157
1161
|
onResponse: failingOnResponse,
|
|
1158
1162
|
claude,
|
package/src/orchestrator.ts
CHANGED
|
@@ -10,17 +10,7 @@ import {
|
|
|
10
10
|
import { writeHistoryPrompt, writeHistoryResult } from "./history";
|
|
11
11
|
import { createLogger } from "./logger";
|
|
12
12
|
import { generateName } from "./naming";
|
|
13
|
-
import {
|
|
14
|
-
backgroundAgentProgressEvent,
|
|
15
|
-
backgroundAgentResultEvent,
|
|
16
|
-
backgroundAgentStartEvent,
|
|
17
|
-
buttonClickEvent,
|
|
18
|
-
healthCheckEvent,
|
|
19
|
-
peekEvent,
|
|
20
|
-
SYSTEM_PROMPT,
|
|
21
|
-
scheduleTriggerEvent,
|
|
22
|
-
userMessageEvent,
|
|
23
|
-
} from "./prompts";
|
|
13
|
+
import { PromptBuilder } from "./prompt-builder";
|
|
24
14
|
import { Queue } from "./queue";
|
|
25
15
|
import { loadSessions, saveSessions } from "./sessions";
|
|
26
16
|
|
|
@@ -100,8 +90,9 @@ interface SessionInfo {
|
|
|
100
90
|
}
|
|
101
91
|
|
|
102
92
|
export interface OrchestratorConfig {
|
|
103
|
-
model
|
|
93
|
+
model: string;
|
|
104
94
|
workspace: string;
|
|
95
|
+
timezone: string;
|
|
105
96
|
settingsDir?: string;
|
|
106
97
|
onResponse: (response: OrchestratorResponse) => Promise<void>;
|
|
107
98
|
claude?: Claude;
|
|
@@ -116,6 +107,7 @@ export interface OrchestratorConfig {
|
|
|
116
107
|
export class Orchestrator {
|
|
117
108
|
#config: Omit<OrchestratorConfig , 'claude'>;
|
|
118
109
|
#claude: Claude;
|
|
110
|
+
#prompts: PromptBuilder;
|
|
119
111
|
#waitThreshold: number;
|
|
120
112
|
#healthCheckInterval: number;
|
|
121
113
|
#healthCheckTimeout: number;
|
|
@@ -127,7 +119,9 @@ export class Orchestrator {
|
|
|
127
119
|
|
|
128
120
|
constructor(config: OrchestratorConfig) {
|
|
129
121
|
this.#config = config;
|
|
130
|
-
this.#
|
|
122
|
+
this.#prompts = new PromptBuilder(config.timezone);
|
|
123
|
+
const envVars: Record<string, string> = { TZ: config.timezone };
|
|
124
|
+
this.#claude = config.claude ?? new Claude({ workspace: config.workspace, systemPrompt: this.#prompts.systemPrompt, envVars });
|
|
131
125
|
this.#waitThreshold = config.waitThreshold ?? WAIT_THRESHOLD;
|
|
132
126
|
this.#healthCheckInterval = config.healthCheckInterval ?? HEALTH_CHECK_INTERVAL_MS;
|
|
133
127
|
this.#healthCheckTimeout = config.healthCheckTimeout ?? HEALTH_CHECK_TIMEOUT_MS;
|
|
@@ -149,7 +143,7 @@ export class Orchestrator {
|
|
|
149
143
|
|
|
150
144
|
handleCron(name: string, prompt: string, model?: string, missed?: { missedBy: string; scheduledAt: string }): void {
|
|
151
145
|
const cronName = `cron-${name}`;
|
|
152
|
-
const formatted =
|
|
146
|
+
const formatted = this.#prompts.scheduleTrigger(
|
|
153
147
|
cronName,
|
|
154
148
|
{ name, missedBy: missed?.missedBy, scheduledAt: missed?.scheduledAt },
|
|
155
149
|
prompt,
|
|
@@ -220,7 +214,7 @@ export class Orchestrator {
|
|
|
220
214
|
this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(session.name)}</b>...` });
|
|
221
215
|
|
|
222
216
|
try {
|
|
223
|
-
const prompt =
|
|
217
|
+
const prompt = this.#prompts.peek(
|
|
224
218
|
`peek-${session.name}`,
|
|
225
219
|
session.name,
|
|
226
220
|
`Only consider progress since the "${session.name}" event. Brief status update: done, in progress, remaining. 2-3 sentences max, plain text.`,
|
|
@@ -438,9 +432,9 @@ export class Orchestrator {
|
|
|
438
432
|
#formatPrompt(request: OrchestratorRequest, name: string, backgroundedEvent?: string): string {
|
|
439
433
|
switch (request.type) {
|
|
440
434
|
case "user":
|
|
441
|
-
return
|
|
435
|
+
return this.#prompts.userMessage(name, request.message || "", { files: request.files, backgroundedEvent });
|
|
442
436
|
case "background-agent-result":
|
|
443
|
-
return
|
|
437
|
+
return this.#prompts.backgroundAgentResult(
|
|
444
438
|
name,
|
|
445
439
|
request.name,
|
|
446
440
|
{ text: request.response.message || "[No output]", files: request.response.files },
|
|
@@ -448,7 +442,7 @@ export class Orchestrator {
|
|
|
448
442
|
{ backgroundedEvent },
|
|
449
443
|
);
|
|
450
444
|
case "background-agent-progress":
|
|
451
|
-
return
|
|
445
|
+
return this.#prompts.backgroundAgentProgress(
|
|
452
446
|
name,
|
|
453
447
|
request.name,
|
|
454
448
|
request.progress,
|
|
@@ -456,7 +450,7 @@ export class Orchestrator {
|
|
|
456
450
|
{ backgroundedEvent },
|
|
457
451
|
);
|
|
458
452
|
case "button":
|
|
459
|
-
return
|
|
453
|
+
return this.#prompts.buttonClick(name, request.label, { backgroundedEvent });
|
|
460
454
|
}
|
|
461
455
|
}
|
|
462
456
|
|
|
@@ -491,7 +485,7 @@ export class Orchestrator {
|
|
|
491
485
|
// --- Background management ---
|
|
492
486
|
|
|
493
487
|
#spawnBackground(name: string, prompt: string, model: string | undefined) {
|
|
494
|
-
const formatted =
|
|
488
|
+
const formatted = this.#prompts.backgroundAgentStart(name, prompt);
|
|
495
489
|
this.#spawnBackgroundRaw(name, prompt, formatted, model);
|
|
496
490
|
}
|
|
497
491
|
|
|
@@ -555,7 +549,7 @@ export class Orchestrator {
|
|
|
555
549
|
|
|
556
550
|
log.debug({ name: info.name, sessionId }, "Running health check");
|
|
557
551
|
|
|
558
|
-
const prompt =
|
|
552
|
+
const prompt = this.#prompts.healthCheck(
|
|
559
553
|
`health-check-${info.name}`,
|
|
560
554
|
info.name,
|
|
561
555
|
"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.",
|
|
@@ -1,75 +1,74 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
healthCheckEvent,
|
|
8
|
-
peekEvent,
|
|
9
|
-
SYSTEM_PROMPT,
|
|
10
|
-
scheduleTriggerEvent,
|
|
11
|
-
userMessageEvent,
|
|
12
|
-
} from "./prompts";
|
|
13
|
-
|
|
14
|
-
describe("SYSTEM_PROMPT", () => {
|
|
2
|
+
import { PromptBuilder } from "./prompt-builder";
|
|
3
|
+
|
|
4
|
+
const p = new PromptBuilder("UTC");
|
|
5
|
+
|
|
6
|
+
describe("systemPrompt", () => {
|
|
15
7
|
it("contains key sections", () => {
|
|
16
|
-
expect(
|
|
17
|
-
expect(
|
|
18
|
-
expect(
|
|
19
|
-
expect(
|
|
20
|
-
expect(
|
|
21
|
-
expect(
|
|
22
|
-
expect(
|
|
23
|
-
expect(
|
|
8
|
+
expect(p.systemPrompt).toContain("macroclaw");
|
|
9
|
+
expect(p.systemPrompt).toContain("Structured output");
|
|
10
|
+
expect(p.systemPrompt).toContain("Event format");
|
|
11
|
+
expect(p.systemPrompt).toContain("Background agents");
|
|
12
|
+
expect(p.systemPrompt).toContain("Cron");
|
|
13
|
+
expect(p.systemPrompt).toContain("Buttons");
|
|
14
|
+
expect(p.systemPrompt).toContain("Files");
|
|
15
|
+
expect(p.systemPrompt).toContain("Session routing");
|
|
24
16
|
});
|
|
25
17
|
|
|
26
18
|
it("contains HTML formatting instructions", () => {
|
|
27
|
-
expect(
|
|
28
|
-
expect(
|
|
19
|
+
expect(p.systemPrompt).toContain("HTML parse mode");
|
|
20
|
+
expect(p.systemPrompt).toContain("<b>");
|
|
29
21
|
});
|
|
30
22
|
|
|
31
23
|
it("documents all event types", () => {
|
|
32
|
-
expect(
|
|
33
|
-
expect(
|
|
34
|
-
expect(
|
|
35
|
-
expect(
|
|
36
|
-
expect(
|
|
37
|
-
expect(
|
|
24
|
+
expect(p.systemPrompt).toContain("user-message");
|
|
25
|
+
expect(p.systemPrompt).toContain("button-click");
|
|
26
|
+
expect(p.systemPrompt).toContain("schedule-trigger");
|
|
27
|
+
expect(p.systemPrompt).toContain("background-agent-start");
|
|
28
|
+
expect(p.systemPrompt).toContain("background-agent-result");
|
|
29
|
+
expect(p.systemPrompt).toContain("peek");
|
|
38
30
|
});
|
|
39
31
|
|
|
40
32
|
it("documents backgrounded events", () => {
|
|
41
|
-
expect(
|
|
42
|
-
expect(
|
|
43
|
-
expect(
|
|
33
|
+
expect(p.systemPrompt).toContain("backgrounded-event");
|
|
34
|
+
expect(p.systemPrompt).toContain("moved to background");
|
|
35
|
+
expect(p.systemPrompt).toContain("Do not re-execute");
|
|
44
36
|
});
|
|
45
37
|
|
|
46
38
|
it("contains structured output reinforcement", () => {
|
|
47
|
-
expect(
|
|
48
|
-
expect(
|
|
39
|
+
expect(p.systemPrompt).toContain("StructuredOutput tool");
|
|
40
|
+
expect(p.systemPrompt).toContain("actionReason");
|
|
49
41
|
});
|
|
50
42
|
|
|
51
43
|
it("contains no personal names", () => {
|
|
52
|
-
expect(
|
|
53
|
-
expect(
|
|
44
|
+
expect(p.systemPrompt).not.toContain("Alfread");
|
|
45
|
+
expect(p.systemPrompt).not.toContain("Michal");
|
|
54
46
|
});
|
|
55
47
|
|
|
56
48
|
it("documents background agent model options", () => {
|
|
57
|
-
expect(
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
49
|
+
expect(p.systemPrompt).toContain("haiku");
|
|
50
|
+
expect(p.systemPrompt).toContain("sonnet");
|
|
51
|
+
expect(p.systemPrompt).toContain("opus");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("includes timezone but not a fixed date", () => {
|
|
55
|
+
const prague = new PromptBuilder("Europe/Prague");
|
|
56
|
+
expect(prague.systemPrompt).not.toContain("Current date:");
|
|
57
|
+
expect(prague.systemPrompt).toContain("Timezone: Europe/Prague");
|
|
58
|
+
expect(prague.systemPrompt).toContain("TZ env var is set");
|
|
60
59
|
});
|
|
61
60
|
});
|
|
62
61
|
|
|
63
|
-
describe("
|
|
64
|
-
it("builds user message event", () => {
|
|
65
|
-
const result =
|
|
66
|
-
expect(result).
|
|
62
|
+
describe("userMessage", () => {
|
|
63
|
+
it("builds user message event with time attribute", () => {
|
|
64
|
+
const result = p.userMessage("check-logs", "hello");
|
|
65
|
+
expect(result).toMatch(/^<event time="\d{4}-\d{2}-\d{2}T\d{2}:\d{2}" name="check-logs" type="user-message" session="main">/);
|
|
67
66
|
expect(result).toContain("<text>hello</text>");
|
|
68
67
|
expect(result).toEndWith("</event>");
|
|
69
68
|
});
|
|
70
69
|
|
|
71
70
|
it("builds user message with files", () => {
|
|
72
|
-
const result =
|
|
71
|
+
const result = p.userMessage("analyze-photo", "what's in this image?", {
|
|
73
72
|
files: ["/tmp/photo.jpg", "/tmp/doc.pdf"],
|
|
74
73
|
});
|
|
75
74
|
expect(result).toContain("<text>what's in this image?</text>");
|
|
@@ -80,7 +79,7 @@ describe("userMessageEvent", () => {
|
|
|
80
79
|
});
|
|
81
80
|
|
|
82
81
|
it("builds user message with backgrounded event", () => {
|
|
83
|
-
const result =
|
|
82
|
+
const result = p.userMessage("check-logs", "check the logs", {
|
|
84
83
|
backgroundedEvent: "deploy-cluster",
|
|
85
84
|
});
|
|
86
85
|
expect(result).toContain('<backgrounded-event name="deploy-cluster" />');
|
|
@@ -88,7 +87,7 @@ describe("userMessageEvent", () => {
|
|
|
88
87
|
});
|
|
89
88
|
|
|
90
89
|
it("places backgrounded-event before text", () => {
|
|
91
|
-
const result =
|
|
90
|
+
const result = p.userMessage("check-logs", "hello", {
|
|
92
91
|
backgroundedEvent: "deploy",
|
|
93
92
|
});
|
|
94
93
|
const bgIdx = result.indexOf("backgrounded-event");
|
|
@@ -97,33 +96,33 @@ describe("userMessageEvent", () => {
|
|
|
97
96
|
});
|
|
98
97
|
|
|
99
98
|
it("escapes XML in text content", () => {
|
|
100
|
-
const result =
|
|
99
|
+
const result = p.userMessage("test", "a < b & c > d");
|
|
101
100
|
expect(result).toContain("<text>a < b & c > d</text>");
|
|
102
101
|
});
|
|
103
102
|
|
|
104
103
|
it("escapes XML in name attribute", () => {
|
|
105
|
-
const result =
|
|
104
|
+
const result = p.userMessage('a & "b"', "test");
|
|
106
105
|
expect(result).toContain('name="a & "b""');
|
|
107
106
|
});
|
|
108
107
|
|
|
109
108
|
it("escapes XML in backgrounded event name", () => {
|
|
110
|
-
const result =
|
|
109
|
+
const result = p.userMessage("test", "hello", {
|
|
111
110
|
backgroundedEvent: 'task & "stuff"',
|
|
112
111
|
});
|
|
113
112
|
expect(result).toContain('backgrounded-event name="task & "stuff""');
|
|
114
113
|
});
|
|
115
114
|
});
|
|
116
115
|
|
|
117
|
-
describe("
|
|
116
|
+
describe("buttonClick", () => {
|
|
118
117
|
it("builds button click event", () => {
|
|
119
|
-
const result =
|
|
118
|
+
const result = p.buttonClick("btn-yes", "Yes");
|
|
120
119
|
expect(result).toContain('type="button-click"');
|
|
121
120
|
expect(result).toContain("<button>Yes</button>");
|
|
122
121
|
expect(result).not.toContain("<text>");
|
|
123
122
|
});
|
|
124
123
|
|
|
125
124
|
it("builds button click with backgrounded event", () => {
|
|
126
|
-
const result =
|
|
125
|
+
const result = p.buttonClick("btn-yes", "Yes", {
|
|
127
126
|
backgroundedEvent: "deploy-cluster",
|
|
128
127
|
});
|
|
129
128
|
expect(result).toContain('<backgrounded-event name="deploy-cluster" />');
|
|
@@ -131,14 +130,14 @@ describe("buttonClickEvent", () => {
|
|
|
131
130
|
});
|
|
132
131
|
|
|
133
132
|
it("escapes XML in button label", () => {
|
|
134
|
-
const result =
|
|
133
|
+
const result = p.buttonClick("btn", 'a & "b"');
|
|
135
134
|
expect(result).toContain("<button>a & "b"</button>");
|
|
136
135
|
});
|
|
137
136
|
});
|
|
138
137
|
|
|
139
|
-
describe("
|
|
138
|
+
describe("scheduleTrigger", () => {
|
|
140
139
|
it("builds schedule trigger event", () => {
|
|
141
|
-
const result =
|
|
140
|
+
const result = p.scheduleTrigger("cron-daily", { name: "daily" }, "check updates");
|
|
142
141
|
expect(result).toContain('type="schedule-trigger"');
|
|
143
142
|
expect(result).toContain('session="background"');
|
|
144
143
|
expect(result).toContain('<schedule name="daily" />');
|
|
@@ -146,7 +145,7 @@ describe("scheduleTriggerEvent", () => {
|
|
|
146
145
|
});
|
|
147
146
|
|
|
148
147
|
it("builds missed schedule trigger with attributes", () => {
|
|
149
|
-
const result =
|
|
148
|
+
const result = p.scheduleTrigger(
|
|
150
149
|
"cron-reminder",
|
|
151
150
|
{ name: "reminder", missedBy: "15m", scheduledAt: "2026-03-20T06:00:00Z" },
|
|
152
151
|
"buy milk",
|
|
@@ -157,18 +156,18 @@ describe("scheduleTriggerEvent", () => {
|
|
|
157
156
|
});
|
|
158
157
|
});
|
|
159
158
|
|
|
160
|
-
describe("
|
|
159
|
+
describe("backgroundAgentStart", () => {
|
|
161
160
|
it("builds background agent start event", () => {
|
|
162
|
-
const result =
|
|
161
|
+
const result = p.backgroundAgentStart("research", "find papers about transformers");
|
|
163
162
|
expect(result).toContain('type="background-agent-start"');
|
|
164
163
|
expect(result).toContain('session="background"');
|
|
165
164
|
expect(result).toContain("<text>find papers about transformers</text>");
|
|
166
165
|
});
|
|
167
166
|
});
|
|
168
167
|
|
|
169
|
-
describe("
|
|
168
|
+
describe("backgroundAgentResult", () => {
|
|
170
169
|
it("builds background agent result (text only)", () => {
|
|
171
|
-
const result =
|
|
170
|
+
const result = p.backgroundAgentResult(
|
|
172
171
|
"bg-research",
|
|
173
172
|
"research",
|
|
174
173
|
{ text: "found 3 papers" },
|
|
@@ -183,7 +182,7 @@ describe("backgroundAgentResultEvent", () => {
|
|
|
183
182
|
});
|
|
184
183
|
|
|
185
184
|
it("builds background agent result with files", () => {
|
|
186
|
-
const result =
|
|
185
|
+
const result = p.backgroundAgentResult(
|
|
187
186
|
"bg-research",
|
|
188
187
|
"research",
|
|
189
188
|
{ text: "here are the screenshots", files: ["/tmp/screenshot.png"] },
|
|
@@ -196,7 +195,7 @@ describe("backgroundAgentResultEvent", () => {
|
|
|
196
195
|
});
|
|
197
196
|
|
|
198
197
|
it("includes instructions after result", () => {
|
|
199
|
-
const result =
|
|
198
|
+
const result = p.backgroundAgentResult(
|
|
200
199
|
"bg-research",
|
|
201
200
|
"research",
|
|
202
201
|
{ text: "done" },
|
|
@@ -210,9 +209,9 @@ describe("backgroundAgentResultEvent", () => {
|
|
|
210
209
|
});
|
|
211
210
|
});
|
|
212
211
|
|
|
213
|
-
describe("
|
|
212
|
+
describe("backgroundAgentProgress", () => {
|
|
214
213
|
it("builds progress event with progress tag", () => {
|
|
215
|
-
const result =
|
|
214
|
+
const result = p.backgroundAgentProgress(
|
|
216
215
|
"progress-research",
|
|
217
216
|
"research",
|
|
218
217
|
"indexing 500 documents",
|
|
@@ -225,9 +224,9 @@ describe("backgroundAgentProgressEvent", () => {
|
|
|
225
224
|
});
|
|
226
225
|
});
|
|
227
226
|
|
|
228
|
-
describe("
|
|
227
|
+
describe("peek", () => {
|
|
229
228
|
it("builds peek event with instructions", () => {
|
|
230
|
-
const result =
|
|
229
|
+
const result = p.peek("peek-deploy", "deploy", "Brief status update.");
|
|
231
230
|
expect(result).toContain('type="peek"');
|
|
232
231
|
expect(result).toContain('<target-event name="deploy" />');
|
|
233
232
|
expect(result).toContain("<instructions>Brief status update.</instructions>");
|
|
@@ -235,9 +234,9 @@ describe("peekEvent", () => {
|
|
|
235
234
|
});
|
|
236
235
|
});
|
|
237
236
|
|
|
238
|
-
describe("
|
|
237
|
+
describe("healthCheck", () => {
|
|
239
238
|
it("builds health check event with instructions", () => {
|
|
240
|
-
const result =
|
|
239
|
+
const result = p.healthCheck("health-check-deploy", "deploy", "Report status.");
|
|
241
240
|
expect(result).toContain('type="health-check"');
|
|
242
241
|
expect(result).toContain('<target-event name="deploy" />');
|
|
243
242
|
expect(result).toContain("<instructions>Report status.</instructions>");
|