macroclaw 0.39.0 → 0.40.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/README.md +2 -2
- package/package.json +1 -1
- package/src/app.test.ts +1 -1
- package/src/app.ts +3 -3
- package/src/index.ts +1 -1
- package/src/orchestrator.test.ts +2 -2
- package/src/orchestrator.ts +3 -3
- package/src/prompt-builder.test.ts +1 -1
- package/src/prompt-builder.ts +5 -5
- package/src/scheduler.test.ts +28 -28
- package/src/scheduler.ts +9 -9
- package/src/settings.test.ts +8 -8
- package/src/settings.ts +2 -2
- package/src/setup.test.ts +2 -2
- package/src/setup.ts +3 -3
- package/workspace-template/.claude/skills/settings/SKILL.md +4 -4
package/README.md
CHANGED
|
@@ -43,12 +43,12 @@ Settings are stored in `~/.macroclaw/settings.json` and validated on startup.
|
|
|
43
43
|
| `chatId` | `AUTHORIZED_CHAT_ID` | — | Yes |
|
|
44
44
|
| `model` | `MODEL` | `sonnet` | No |
|
|
45
45
|
| `workspace` | `WORKSPACE` | `~/.macroclaw-workspace` | No |
|
|
46
|
-
| `
|
|
46
|
+
| `timeZone` | `TIMEZONE` | `UTC` | No |
|
|
47
47
|
| `openaiApiKey` | `OPENAI_API_KEY` | — | No |
|
|
48
48
|
| `logLevel` | `LOG_LEVEL` | `info` | No |
|
|
49
49
|
| `pinoramaUrl` | `PINORAMA_URL` | — | No |
|
|
50
50
|
|
|
51
|
-
**`
|
|
51
|
+
**`timeZone`** sets the agent's local time zone (IANA format, e.g. `Europe/Prague`, `America/New_York`). Used for the agent's clock display and scheduled event timing.
|
|
52
52
|
|
|
53
53
|
**`openaiApiKey`** is used for voice message transcription via [OpenAI Whisper](https://platform.openai.com/docs/guides/speech-to-text). Without it, voice messages are ignored.
|
|
54
54
|
|
package/package.json
CHANGED
package/src/app.test.ts
CHANGED
|
@@ -133,7 +133,7 @@ function makeConfig(overrides?: Partial<AppConfig>): AppConfig {
|
|
|
133
133
|
authorizedChatId: "12345",
|
|
134
134
|
workspace: "/tmp/macroclaw-test-workspace",
|
|
135
135
|
model: "sonnet",
|
|
136
|
-
|
|
136
|
+
timeZone: "UTC",
|
|
137
137
|
settingsDir: tmpSettingsDir,
|
|
138
138
|
claude: defaultMockClaude(),
|
|
139
139
|
stt: mockStt(),
|
package/src/app.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface AppConfig {
|
|
|
12
12
|
authorizedChatId: string;
|
|
13
13
|
workspace: string;
|
|
14
14
|
model: string;
|
|
15
|
-
|
|
15
|
+
timeZone: string;
|
|
16
16
|
settingsDir?: string;
|
|
17
17
|
claude?: Claude;
|
|
18
18
|
stt?: SpeechToText;
|
|
@@ -30,7 +30,7 @@ export class App {
|
|
|
30
30
|
this.#orchestrator = new Orchestrator({
|
|
31
31
|
model: config.model,
|
|
32
32
|
workspace: config.workspace,
|
|
33
|
-
|
|
33
|
+
timeZone: config.timeZone,
|
|
34
34
|
settingsDir: config.settingsDir,
|
|
35
35
|
claude: config.claude,
|
|
36
36
|
healthCheckInterval: config.healthCheckInterval,
|
|
@@ -51,7 +51,7 @@ export class App {
|
|
|
51
51
|
start() {
|
|
52
52
|
log.info("Starting macroclaw...");
|
|
53
53
|
const scheduler = new Scheduler(this.#config.workspace, {
|
|
54
|
-
|
|
54
|
+
timeZone: this.#config.timeZone,
|
|
55
55
|
onJob: (name, prompt, model, missed) => this.#orchestrator.handleCron(name, prompt, model, missed),
|
|
56
56
|
});
|
|
57
57
|
scheduler.start();
|
package/src/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export async function start(): Promise<void> {
|
|
|
40
40
|
authorizedChatId: resolved.chatId,
|
|
41
41
|
workspace,
|
|
42
42
|
model: resolved.model,
|
|
43
|
-
|
|
43
|
+
timeZone: resolved.timeZone,
|
|
44
44
|
stt: resolved.openaiApiKey ? new SpeechToText(resolved.openaiApiKey) : undefined,
|
|
45
45
|
};
|
|
46
46
|
|
package/src/orchestrator.test.ts
CHANGED
|
@@ -117,7 +117,7 @@ function makeOrchestrator(claude: Claude, extraConfig?: Partial<OrchestratorConf
|
|
|
117
117
|
const orch = new Orchestrator({
|
|
118
118
|
workspace: TEST_WORKSPACE,
|
|
119
119
|
model: "sonnet",
|
|
120
|
-
|
|
120
|
+
timeZone: "UTC",
|
|
121
121
|
settingsDir: tmpSettingsDir,
|
|
122
122
|
onResponse,
|
|
123
123
|
claude,
|
|
@@ -1156,7 +1156,7 @@ describe("Orchestrator", () => {
|
|
|
1156
1156
|
const orch = new Orchestrator({
|
|
1157
1157
|
workspace: TEST_WORKSPACE,
|
|
1158
1158
|
model: "sonnet",
|
|
1159
|
-
|
|
1159
|
+
timeZone: "UTC",
|
|
1160
1160
|
settingsDir: tmpSettingsDir,
|
|
1161
1161
|
onResponse: failingOnResponse,
|
|
1162
1162
|
claude,
|
package/src/orchestrator.ts
CHANGED
|
@@ -92,7 +92,7 @@ interface SessionInfo {
|
|
|
92
92
|
export interface OrchestratorConfig {
|
|
93
93
|
model: string;
|
|
94
94
|
workspace: string;
|
|
95
|
-
|
|
95
|
+
timeZone: string;
|
|
96
96
|
settingsDir?: string;
|
|
97
97
|
onResponse: (response: OrchestratorResponse) => Promise<void>;
|
|
98
98
|
claude?: Claude;
|
|
@@ -119,8 +119,8 @@ export class Orchestrator {
|
|
|
119
119
|
|
|
120
120
|
constructor(config: OrchestratorConfig) {
|
|
121
121
|
this.#config = config;
|
|
122
|
-
this.#prompts = new PromptBuilder(config.
|
|
123
|
-
const envVars: Record<string, string> = { TZ: config.
|
|
122
|
+
this.#prompts = new PromptBuilder(config.timeZone);
|
|
123
|
+
const envVars: Record<string, string> = { TZ: config.timeZone };
|
|
124
124
|
this.#claude = config.claude ?? new Claude({ workspace: config.workspace, systemPrompt: this.#prompts.systemPrompt, envVars });
|
|
125
125
|
this.#waitThreshold = config.waitThreshold ?? WAIT_THRESHOLD;
|
|
126
126
|
this.#healthCheckInterval = config.healthCheckInterval ?? HEALTH_CHECK_INTERVAL_MS;
|
|
@@ -51,7 +51,7 @@ describe("systemPrompt", () => {
|
|
|
51
51
|
expect(p.systemPrompt).toContain("opus");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it("includes
|
|
54
|
+
it("includes time zone but not a fixed date", () => {
|
|
55
55
|
const prague = new PromptBuilder("Europe/Prague");
|
|
56
56
|
expect(prague.systemPrompt).not.toContain("Current date:");
|
|
57
57
|
expect(prague.systemPrompt).toContain("Timezone: Europe/Prague");
|
package/src/prompt-builder.ts
CHANGED
|
@@ -82,18 +82,18 @@ interface BuildXmlFields {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export class PromptBuilder {
|
|
85
|
-
readonly #
|
|
85
|
+
readonly #timeZone: string;
|
|
86
86
|
|
|
87
|
-
constructor(
|
|
88
|
-
this.#
|
|
87
|
+
constructor(timeZone: string) {
|
|
88
|
+
this.#timeZone = timeZone;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
get systemPrompt(): string {
|
|
92
|
-
return `${SYSTEM_PROMPT_BASE}\n\nTimezone: ${this.#
|
|
92
|
+
return `${SYSTEM_PROMPT_BASE}\n\nTimezone: ${this.#timeZone}. TZ env var is set — \`date\` and other CLI tools return local time.`;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
#localTime(): string {
|
|
96
|
-
return DateTime.now().setZone(this.#
|
|
96
|
+
return DateTime.now().setZone(this.#timeZone).toFormat("yyyy-MM-dd'T'HH:mm");
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
static #escapeXml(text: string): string {
|
package/src/scheduler.test.ts
CHANGED
|
@@ -73,7 +73,7 @@ describe("Scheduler — cron jobs", () => {
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
const onJob = makeOnJob();
|
|
76
|
-
const s = new Scheduler(TEST_DIR, {
|
|
76
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
77
77
|
s.start();
|
|
78
78
|
s.stop();
|
|
79
79
|
|
|
@@ -86,7 +86,7 @@ describe("Scheduler — cron jobs", () => {
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
const onJob = makeOnJob();
|
|
89
|
-
const s = new Scheduler(TEST_DIR, {
|
|
89
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
90
90
|
s.start();
|
|
91
91
|
s.stop();
|
|
92
92
|
|
|
@@ -102,7 +102,7 @@ describe("Scheduler — cron jobs", () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
const onJob = makeOnJob();
|
|
105
|
-
const s = new Scheduler(TEST_DIR, {
|
|
105
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
106
106
|
s.start();
|
|
107
107
|
s.stop();
|
|
108
108
|
|
|
@@ -119,7 +119,7 @@ describe("Scheduler — cron jobs", () => {
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
const onJob = makeOnJob();
|
|
122
|
-
const s = new Scheduler(TEST_DIR, {
|
|
122
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
123
123
|
s.start();
|
|
124
124
|
s.stop();
|
|
125
125
|
|
|
@@ -134,7 +134,7 @@ describe("Scheduler — cron jobs", () => {
|
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
const onJob = makeOnJob();
|
|
137
|
-
const s = new Scheduler(TEST_DIR, {
|
|
137
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
138
138
|
s.start();
|
|
139
139
|
s.stop();
|
|
140
140
|
|
|
@@ -147,7 +147,7 @@ describe("Scheduler — cron jobs", () => {
|
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
const onJob = makeOnJob();
|
|
150
|
-
const s = new Scheduler(TEST_DIR, {
|
|
150
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
151
151
|
s.start();
|
|
152
152
|
s.stop();
|
|
153
153
|
|
|
@@ -164,7 +164,7 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
const onJob = makeOnJob();
|
|
167
|
-
const s = new Scheduler(TEST_DIR, {
|
|
167
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
168
168
|
s.start();
|
|
169
169
|
s.stop();
|
|
170
170
|
|
|
@@ -181,7 +181,7 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
181
181
|
});
|
|
182
182
|
|
|
183
183
|
const onJob = makeOnJob();
|
|
184
|
-
const s = new Scheduler(TEST_DIR, {
|
|
184
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
185
185
|
s.start();
|
|
186
186
|
s.stop();
|
|
187
187
|
|
|
@@ -197,7 +197,7 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
197
197
|
});
|
|
198
198
|
|
|
199
199
|
const onJob = makeOnJob();
|
|
200
|
-
const s = new Scheduler(TEST_DIR, {
|
|
200
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
201
201
|
s.start();
|
|
202
202
|
s.stop();
|
|
203
203
|
|
|
@@ -214,7 +214,7 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
const onJob = makeOnJob();
|
|
217
|
-
const s = new Scheduler(TEST_DIR, {
|
|
217
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
218
218
|
s.start();
|
|
219
219
|
s.stop();
|
|
220
220
|
|
|
@@ -236,7 +236,7 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
const onJob = makeOnJob();
|
|
239
|
-
const s = new Scheduler(TEST_DIR, {
|
|
239
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
240
240
|
s.start();
|
|
241
241
|
s.stop();
|
|
242
242
|
|
|
@@ -252,7 +252,7 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
252
252
|
});
|
|
253
253
|
|
|
254
254
|
const onJob = makeOnJob();
|
|
255
|
-
const s = new Scheduler(TEST_DIR, {
|
|
255
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
256
256
|
s.start();
|
|
257
257
|
s.stop();
|
|
258
258
|
|
|
@@ -273,7 +273,7 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
273
273
|
});
|
|
274
274
|
|
|
275
275
|
const onJob = makeOnJob();
|
|
276
|
-
const s = new Scheduler(TEST_DIR, {
|
|
276
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
277
277
|
s.start();
|
|
278
278
|
s.stop();
|
|
279
279
|
|
|
@@ -290,14 +290,14 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
const onJob = makeOnJob();
|
|
293
|
-
const s = new Scheduler(TEST_DIR, {
|
|
293
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
294
294
|
s.start();
|
|
295
295
|
s.stop();
|
|
296
296
|
|
|
297
297
|
expect(onJob).toHaveBeenCalledWith("smart", "think", "opus");
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
-
it("interprets offset-less fireAt in the configured
|
|
300
|
+
it("interprets offset-less fireAt in the configured time zone", () => {
|
|
301
301
|
// Create a fireAt 30 seconds ago in Europe/Prague local time (no offset)
|
|
302
302
|
const now = new Date();
|
|
303
303
|
const pragueStr = now.toLocaleString("en-US", { timeZone: "Europe/Prague" });
|
|
@@ -312,21 +312,21 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
312
312
|
});
|
|
313
313
|
|
|
314
314
|
const onJob = makeOnJob();
|
|
315
|
-
const s = new Scheduler(TEST_DIR, {
|
|
315
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "Europe/Prague", onJob });
|
|
316
316
|
s.start();
|
|
317
317
|
s.stop();
|
|
318
318
|
|
|
319
319
|
expect(onJob).toHaveBeenCalledWith("local", "local time", undefined);
|
|
320
320
|
});
|
|
321
321
|
|
|
322
|
-
it("preserves explicit offset in fireAt (ignores configured
|
|
322
|
+
it("preserves explicit offset in fireAt (ignores configured time zone)", () => {
|
|
323
323
|
// fireAt with explicit +00:00 offset, 30s ago — should fire regardless of configured tz
|
|
324
324
|
writeScheduleConfig({
|
|
325
325
|
jobs: [{ name: "explicit", fireAt: justNowFireAt(), prompt: "with offset" }],
|
|
326
326
|
});
|
|
327
327
|
|
|
328
328
|
const onJob = makeOnJob();
|
|
329
|
-
const s = new Scheduler(TEST_DIR, {
|
|
329
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "Asia/Tokyo", onJob });
|
|
330
330
|
s.start();
|
|
331
331
|
s.stop();
|
|
332
332
|
|
|
@@ -342,7 +342,7 @@ describe("Scheduler — fireAt jobs", () => {
|
|
|
342
342
|
chmodSync(SCHEDULE_FILE, 0o444);
|
|
343
343
|
|
|
344
344
|
const onJob = makeOnJob();
|
|
345
|
-
const s = new Scheduler(TEST_DIR, {
|
|
345
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
346
346
|
s.start();
|
|
347
347
|
s.stop();
|
|
348
348
|
|
|
@@ -357,7 +357,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
357
357
|
rmSync(SCHEDULE_FILE, { force: true });
|
|
358
358
|
|
|
359
359
|
const onJob = makeOnJob();
|
|
360
|
-
const s = new Scheduler(TEST_DIR, {
|
|
360
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
361
361
|
s.start();
|
|
362
362
|
s.stop();
|
|
363
363
|
|
|
@@ -368,7 +368,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
368
368
|
writeFileSync(SCHEDULE_FILE, "not json{{{");
|
|
369
369
|
|
|
370
370
|
const onJob = makeOnJob();
|
|
371
|
-
const s = new Scheduler(TEST_DIR, {
|
|
371
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
372
372
|
s.start();
|
|
373
373
|
s.stop();
|
|
374
374
|
|
|
@@ -379,7 +379,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
379
379
|
writeScheduleConfig({ jobs: "not-array" });
|
|
380
380
|
|
|
381
381
|
const onJob = makeOnJob();
|
|
382
|
-
const s = new Scheduler(TEST_DIR, {
|
|
382
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
383
383
|
s.start();
|
|
384
384
|
s.stop();
|
|
385
385
|
|
|
@@ -392,7 +392,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
const onJob = makeOnJob();
|
|
395
|
-
const s = new Scheduler(TEST_DIR, {
|
|
395
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
396
396
|
s.start();
|
|
397
397
|
s.stop();
|
|
398
398
|
|
|
@@ -405,7 +405,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
405
405
|
});
|
|
406
406
|
|
|
407
407
|
const onJob = makeOnJob();
|
|
408
|
-
const s = new Scheduler(TEST_DIR, {
|
|
408
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
409
409
|
s.start();
|
|
410
410
|
s.stop();
|
|
411
411
|
|
|
@@ -420,7 +420,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
420
420
|
writeScheduleConfig({ jobs: [] });
|
|
421
421
|
|
|
422
422
|
const onJob = makeOnJob();
|
|
423
|
-
const s = new Scheduler(TEST_DIR, {
|
|
423
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
424
424
|
s.start();
|
|
425
425
|
s.stop(); // should not throw
|
|
426
426
|
});
|
|
@@ -431,7 +431,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
431
431
|
});
|
|
432
432
|
|
|
433
433
|
const onJob = makeOnJob();
|
|
434
|
-
const s = new Scheduler(TEST_DIR, {
|
|
434
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
435
435
|
s.start();
|
|
436
436
|
s.stop();
|
|
437
437
|
|
|
@@ -439,7 +439,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
439
439
|
|
|
440
440
|
// Start again with a new instance — the lastMinute tracker is per-instance
|
|
441
441
|
const onJob2 = makeOnJob();
|
|
442
|
-
const s2 = new Scheduler(TEST_DIR, {
|
|
442
|
+
const s2 = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob: onJob2 });
|
|
443
443
|
s2.start();
|
|
444
444
|
s2.stop();
|
|
445
445
|
|
|
@@ -452,7 +452,7 @@ describe("Scheduler — validation and edge cases", () => {
|
|
|
452
452
|
});
|
|
453
453
|
|
|
454
454
|
const onJob = makeOnJob();
|
|
455
|
-
const s = new Scheduler(TEST_DIR, {
|
|
455
|
+
const s = new Scheduler(TEST_DIR, { timeZone: "UTC", onJob });
|
|
456
456
|
s.start();
|
|
457
457
|
s.stop();
|
|
458
458
|
|
package/src/scheduler.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface MissedInfo {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface SchedulerConfig {
|
|
31
|
-
|
|
31
|
+
timeZone: string;
|
|
32
32
|
onJob: (name: string, prompt: string, model?: string, missed?: MissedInfo) => void;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -39,13 +39,13 @@ export class Scheduler {
|
|
|
39
39
|
#lastMinute = -1;
|
|
40
40
|
#schedulePath: string;
|
|
41
41
|
#config: SchedulerConfig;
|
|
42
|
-
#
|
|
42
|
+
#timeZone: string;
|
|
43
43
|
#timer: Timer | null = null;
|
|
44
44
|
|
|
45
45
|
constructor(workspace: string, config: SchedulerConfig) {
|
|
46
46
|
this.#schedulePath = join(workspace, "data", "schedule.json");
|
|
47
47
|
this.#config = config;
|
|
48
|
-
this.#
|
|
48
|
+
this.#timeZone = config.timeZone;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
start(): void {
|
|
@@ -120,7 +120,7 @@ export class Scheduler {
|
|
|
120
120
|
|
|
121
121
|
#evaluateCronJob(job: { name: string; cron: string; prompt: string; model?: string }, now: Date): void {
|
|
122
122
|
try {
|
|
123
|
-
const interval = CronExpressionParser.parse(job.cron, { tz: this.#
|
|
123
|
+
const interval = CronExpressionParser.parse(job.cron, { tz: this.#timeZone });
|
|
124
124
|
const prev = interval.prev();
|
|
125
125
|
const diff = Math.abs(now.getTime() - prev.getTime());
|
|
126
126
|
if (diff < 60_000) {
|
|
@@ -132,22 +132,22 @@ export class Scheduler {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/** Parse a fireAt string, interpreting offset-less timestamps in the given
|
|
136
|
-
static #parseFireAt(fireAt: string,
|
|
135
|
+
/** Parse a fireAt string, interpreting offset-less timestamps in the given time zone. */
|
|
136
|
+
static #parseFireAt(fireAt: string, timeZone: string): Date {
|
|
137
137
|
const probe = DateTime.fromISO(fireAt, { setZone: true });
|
|
138
138
|
if (probe.isValid && probe.isOffsetFixed) {
|
|
139
139
|
// Has explicit offset (Z, +HH:MM, etc.) — use as-is
|
|
140
140
|
return probe.toJSDate();
|
|
141
141
|
}
|
|
142
|
-
// No offset — interpret as local time in the configured
|
|
143
|
-
return DateTime.fromISO(fireAt, { zone:
|
|
142
|
+
// No offset — interpret as local time in the configured time zone
|
|
143
|
+
return DateTime.fromISO(fireAt, { zone: timeZone }).toJSDate();
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
#evaluateFireAtJob(
|
|
147
147
|
job: { name: string; fireAt: string; prompt: string; model?: string },
|
|
148
148
|
now: Date,
|
|
149
149
|
): "remove" | "keep" {
|
|
150
|
-
const fireAt = Scheduler.#parseFireAt(job.fireAt, this.#
|
|
150
|
+
const fireAt = Scheduler.#parseFireAt(job.fireAt, this.#timeZone);
|
|
151
151
|
if (Number.isNaN(fireAt.getTime())) {
|
|
152
152
|
log.warn({ name: job.name, fireAt: job.fireAt }, "Invalid fireAt date");
|
|
153
153
|
return "keep";
|
package/src/settings.test.ts
CHANGED
|
@@ -10,7 +10,7 @@ const validSettings: Settings = {
|
|
|
10
10
|
chatId: "12345678",
|
|
11
11
|
model: "sonnet",
|
|
12
12
|
workspace: "~/.macroclaw-workspace",
|
|
13
|
-
|
|
13
|
+
timeZone: "UTC",
|
|
14
14
|
logLevel: "debug",
|
|
15
15
|
};
|
|
16
16
|
|
|
@@ -40,7 +40,7 @@ describe("SettingsManager.load", () => {
|
|
|
40
40
|
chatId: "12345678",
|
|
41
41
|
model: "sonnet",
|
|
42
42
|
workspace: "~/.macroclaw-workspace",
|
|
43
|
-
|
|
43
|
+
timeZone: "UTC",
|
|
44
44
|
logLevel: "info",
|
|
45
45
|
});
|
|
46
46
|
});
|
|
@@ -52,7 +52,7 @@ describe("SettingsManager.load", () => {
|
|
|
52
52
|
chatId: " 12345678 ",
|
|
53
53
|
model: " opus ",
|
|
54
54
|
workspace: " /custom/workspace ",
|
|
55
|
-
|
|
55
|
+
timeZone: " Europe/Prague ",
|
|
56
56
|
openaiApiKey: " sk-test ",
|
|
57
57
|
logLevel: " warn ",
|
|
58
58
|
pinoramaUrl: " http://localhost:6200 ",
|
|
@@ -63,7 +63,7 @@ describe("SettingsManager.load", () => {
|
|
|
63
63
|
chatId: "12345678",
|
|
64
64
|
model: "opus",
|
|
65
65
|
workspace: "/custom/workspace",
|
|
66
|
-
|
|
66
|
+
timeZone: "Europe/Prague",
|
|
67
67
|
openaiApiKey: "sk-test",
|
|
68
68
|
logLevel: "warn",
|
|
69
69
|
pinoramaUrl: "http://localhost:6200",
|
|
@@ -87,7 +87,7 @@ describe("SettingsManager.load", () => {
|
|
|
87
87
|
chatId: "123",
|
|
88
88
|
model: "opus",
|
|
89
89
|
workspace: "/custom",
|
|
90
|
-
|
|
90
|
+
timeZone: "UTC",
|
|
91
91
|
openaiApiKey: "sk-test",
|
|
92
92
|
logLevel: "info",
|
|
93
93
|
pinoramaUrl: "http://localhost:6200",
|
|
@@ -152,12 +152,12 @@ describe("SettingsManager.load", () => {
|
|
|
152
152
|
process.exit = origExit;
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
it("exits with code 1 when validation fails (invalid
|
|
155
|
+
it("exits with code 1 when validation fails (invalid timeZone)", () => {
|
|
156
156
|
mkdirSync(tmpDir, { recursive: true });
|
|
157
157
|
writeFileSync(join(tmpDir, "settings.json"), JSON.stringify({
|
|
158
158
|
botToken: "tok",
|
|
159
159
|
chatId: "123",
|
|
160
|
-
|
|
160
|
+
timeZone: "Europe/Prgaaue",
|
|
161
161
|
}));
|
|
162
162
|
|
|
163
163
|
const mockExit = mock(() => { throw new Error("exit"); });
|
|
@@ -283,7 +283,7 @@ describe("SettingsManager.applyEnvOverrides", () => {
|
|
|
283
283
|
const { settings } = new SettingsManager(tmpDir).applyEnvOverrides(validSettings);
|
|
284
284
|
expect(settings.model).toBe("opus");
|
|
285
285
|
expect(settings.workspace).toBe("/override/path");
|
|
286
|
-
expect(settings.
|
|
286
|
+
expect(settings.timeZone).toBe("Europe/Prague");
|
|
287
287
|
expect(settings.logLevel).toBe("error");
|
|
288
288
|
expect(settings.pinoramaUrl).toBe("http://override:6200");
|
|
289
289
|
});
|
package/src/settings.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const settingsSchema = z.object({
|
|
|
11
11
|
chatId: z.string().trim().regex(/^-?\d+$/, "Must be a numeric Telegram chat ID"),
|
|
12
12
|
model: z.string().trim().pipe(z.enum(["haiku", "sonnet", "opus"])).default("sonnet"),
|
|
13
13
|
workspace: z.string().trim().default("~/.macroclaw-workspace"),
|
|
14
|
-
|
|
14
|
+
timeZone: z.string().trim().refine((tz) => IANAZone.isValidZone(tz), "Must be a valid IANA timezone").default("UTC"),
|
|
15
15
|
openaiApiKey: z.string().trim().optional(),
|
|
16
16
|
logLevel: z.string().trim().pipe(z.enum(["debug", "info", "warn", "error"])).default("info"),
|
|
17
17
|
pinoramaUrl: z.string().trim().optional(),
|
|
@@ -37,7 +37,7 @@ export class SettingsManager {
|
|
|
37
37
|
chatId: "AUTHORIZED_CHAT_ID",
|
|
38
38
|
model: "MODEL",
|
|
39
39
|
workspace: "WORKSPACE",
|
|
40
|
-
|
|
40
|
+
timeZone: "TIMEZONE",
|
|
41
41
|
openaiApiKey: "OPENAI_API_KEY",
|
|
42
42
|
logLevel: "LOG_LEVEL",
|
|
43
43
|
pinoramaUrl: "PINORAMA_URL",
|
package/src/setup.test.ts
CHANGED
|
@@ -122,7 +122,7 @@ describe("SetupWizard", () => {
|
|
|
122
122
|
expect(settings.chatId).toBe("12345678");
|
|
123
123
|
expect(settings.model).toBe("opus");
|
|
124
124
|
expect(settings.workspace).toBe("/my/ws");
|
|
125
|
-
expect(settings.
|
|
125
|
+
expect(settings.timeZone).toBe("Europe/Prague");
|
|
126
126
|
expect(settings.openaiApiKey).toBe("sk-test");
|
|
127
127
|
expect(settings.logLevel).toBe("info");
|
|
128
128
|
});
|
|
@@ -151,7 +151,7 @@ describe("SetupWizard", () => {
|
|
|
151
151
|
expect(settings.chatId).toBe("99887766");
|
|
152
152
|
expect(settings.model).toBe("haiku");
|
|
153
153
|
expect(settings.workspace).toBe("/env/ws");
|
|
154
|
-
expect(settings.
|
|
154
|
+
expect(settings.timeZone).toBe("America/New_York");
|
|
155
155
|
expect(settings.openaiApiKey).toBe("sk-env");
|
|
156
156
|
});
|
|
157
157
|
|
package/src/setup.ts
CHANGED
|
@@ -88,8 +88,8 @@ export class SetupWizard {
|
|
|
88
88
|
this.#io.write("\nLocal timezone for the agent's clock and scheduled events.\n");
|
|
89
89
|
this.#io.write("Use an IANA timezone name (e.g. Europe/Prague, America/New_York, UTC).\n\n");
|
|
90
90
|
const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
91
|
-
const defaultTimezone = this.#default("
|
|
92
|
-
const
|
|
91
|
+
const defaultTimezone = this.#default("timeZone", detectedTz || "UTC");
|
|
92
|
+
const timeZone = await this.#askValidated("timeZone", `Timezone [${defaultTimezone}]: `, defaultTimezone);
|
|
93
93
|
|
|
94
94
|
// OpenAI API key
|
|
95
95
|
this.#io.write("\nMacroclaw uses OpenAI's Whisper API to transcribe voice messages.\n");
|
|
@@ -106,7 +106,7 @@ export class SetupWizard {
|
|
|
106
106
|
chatId,
|
|
107
107
|
model,
|
|
108
108
|
workspace,
|
|
109
|
-
|
|
109
|
+
timeZone,
|
|
110
110
|
openaiApiKey,
|
|
111
111
|
...(logLevel && { logLevel }),
|
|
112
112
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: settings
|
|
3
|
-
description: "Read or change macroclaw settings (model,
|
|
3
|
+
description: "Read or change macroclaw settings (model, timeZone). Use when the user asks about current settings, wants to switch the Claude model, change the timezone, or asks what model/timezone is configured."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Read or change macroclaw settings. Only `model` and `
|
|
6
|
+
Read or change macroclaw settings. Only `model` and `timeZone` can be changed through this skill.
|
|
7
7
|
|
|
8
8
|
## Settings file
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ Location: `~/.macroclaw/settings.json`
|
|
|
11
11
|
|
|
12
12
|
## Reading settings
|
|
13
13
|
|
|
14
|
-
When the user asks about current settings ("what model am I on?", "what's the
|
|
14
|
+
When the user asks about current settings ("what model am I on?", "what's the time zone?"):
|
|
15
15
|
|
|
16
16
|
1. Read `~/.macroclaw/settings.json`
|
|
17
17
|
2. Report the requested value
|
|
@@ -20,7 +20,7 @@ When the user asks about current settings ("what model am I on?", "what's the ti
|
|
|
20
20
|
|
|
21
21
|
Allowed changes:
|
|
22
22
|
- **model**: `haiku`, `sonnet`, or `opus`
|
|
23
|
-
- **
|
|
23
|
+
- **timeZone**: any valid IANA timezone (e.g. `Europe/Prague`, `America/New_York`, `UTC`)
|
|
24
24
|
|
|
25
25
|
All other settings (botToken, chatId, workspace, etc.) cannot be changed through this skill — tell the user to run `macroclaw setup` instead.
|
|
26
26
|
|