macroclaw 0.35.0 → 0.37.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 CHANGED
@@ -25,7 +25,7 @@ bunx macroclaw setup
25
25
  This runs the setup wizard, which:
26
26
  1. Asks for your **Telegram bot token** (from [@BotFather](https://t.me/BotFather))
27
27
  2. Starts the bot temporarily so you can send `/chatid` to discover your chat ID
28
- 3. Asks for your **chat ID**, **model** preference, **workspace path**, and optional **OpenAI API key**
28
+ 3. Asks for your **chat ID**, **model** preference, **workspace path**, **timezone**, and optional **OpenAI API key**
29
29
  4. Saves settings to `~/.macroclaw/settings.json`
30
30
  5. Offers to install as a system service — this installs macroclaw globally (`bun install -g`), registers it as a **launchd** agent (macOS) or **systemd** unit (Linux), and starts the bridge automatically
31
31
 
@@ -43,10 +43,13 @@ 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
+ | `timezone` | `TIMEZONE` | `UTC` | No |
46
47
  | `openaiApiKey` | `OPENAI_API_KEY` | — | No |
47
- | `logLevel` | `LOG_LEVEL` | `debug` | No |
48
+ | `logLevel` | `LOG_LEVEL` | `info` | No |
48
49
  | `pinoramaUrl` | `PINORAMA_URL` | — | No |
49
50
 
51
+ **`timezone`** sets the agent's local timezone (IANA format, e.g. `Europe/Prague`, `America/New_York`). Used for the agent's clock display and scheduled event timing.
52
+
50
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.
51
54
 
52
55
  Env vars take precedence over settings file values. On startup, a masked settings summary is printed showing which values were overridden by env vars.
@@ -66,7 +69,10 @@ Run `macroclaw --help` or `macroclaw <command> --help` for the complete referenc
66
69
  | `macroclaw service uninstall` | Stop and remove the system service |
67
70
  | `macroclaw service start` | Start the system service |
68
71
  | `macroclaw service stop` | Stop the system service |
72
+ | `macroclaw service restart` | Restart the system service |
69
73
  | `macroclaw service update` | Reinstall latest version and restart |
74
+ | `macroclaw service status` | Show service installation and running status |
75
+ | `macroclaw service logs` | Print the command to view service logs |
70
76
 
71
77
  ### Running as a service
72
78
 
@@ -80,7 +86,7 @@ macroclaw service install
80
86
 
81
87
  Both paths install macroclaw globally via `bun install -g`, register it as a **launchd** agent (macOS) or **systemd** unit (Linux) with auto-restart, and start the bridge.
82
88
 
83
- On Linux, the command runs as a normal user. Only the privileged operations (writing to `/etc/systemd/system/`, systemctl commands) are elevated via `sudo`, which prompts for a password when needed. Package installation and path resolution stay in the user's environment.
89
+ On Linux, the service is installed as a **systemd user unit** (`~/.config/systemd/user/`). Only `loginctl enable-linger` is elevated via `sudo` (so the service runs without an active login session). All other operations run unprivileged.
84
90
 
85
91
  ## Docker
86
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macroclaw",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Telegram-to-Claude-Code bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
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,
@@ -641,6 +641,50 @@ describe("stop", () => {
641
641
  });
642
642
  });
643
643
 
644
+ describe("restart", () => {
645
+ it("throws when service is not installed", () => {
646
+ const mgr = createManager({ platform: "darwin", home: "/nonexistent" });
647
+ expect(() => mgr.restart()).toThrow(
648
+ "Service not installed. Run `macroclaw service install` first.",
649
+ );
650
+ });
651
+
652
+ it("stops then starts when running (launchd)", () => {
653
+ const tmpHome = `/tmp/macroclaw-test-restartld-${Date.now()}`;
654
+ mkdirSync(join(tmpHome, "Library/LaunchAgents"), { recursive: true });
655
+ writeFileSync(join(tmpHome, "Library/LaunchAgents/com.macroclaw.plist"), "test");
656
+
657
+ let stopped = false;
658
+ mockExecSync.mockImplementation((cmd: string) => {
659
+ if (cmd.startsWith("launchctl list ")) return stopped ? LAUNCHD_STOPPED : LAUNCHD_RUNNING;
660
+ if (cmd.includes("launchctl unload")) { stopped = true; return ""; }
661
+ return "";
662
+ });
663
+ const mgr = createManager({ platform: "darwin", home: tmpHome });
664
+ mgr.restart();
665
+ expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl unload"), expect.anything());
666
+ expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining("launchctl load"), expect.anything());
667
+ rmSync(tmpHome, { recursive: true });
668
+ });
669
+
670
+ it("skips stop and starts when not running (systemd)", () => {
671
+ const tmpHome = `/tmp/macroclaw-test-restartsys-${Date.now()}`;
672
+ const unitDir = join(tmpHome, ".config/systemd/user");
673
+ mkdirSync(unitDir, { recursive: true });
674
+ writeFileSync(join(unitDir, "macroclaw.service"), "test");
675
+
676
+ mockExecSync.mockImplementation((cmd: string) => {
677
+ if (cmd === "systemctl --user is-active macroclaw") throw new Error("inactive");
678
+ return "";
679
+ });
680
+ const mgr = createManager({ platform: "linux", home: tmpHome });
681
+ mgr.restart();
682
+ expect(mockExecSync).not.toHaveBeenCalledWith("systemctl --user stop macroclaw", expect.anything());
683
+ expect(mockExecSync).toHaveBeenCalledWith("systemctl --user start macroclaw", expect.anything());
684
+ rmSync(tmpHome, { recursive: true });
685
+ });
686
+ });
687
+
644
688
  describe("update", () => {
645
689
  it("throws when service is not installed", () => {
646
690
  const mgr = createManager({ platform: "darwin", home: "/nonexistent" });
@@ -171,6 +171,16 @@ export class SystemServiceManager {
171
171
  return this.#logTailCommand();
172
172
  }
173
173
 
174
+ restart(): string {
175
+ this.#requireInstalled();
176
+
177
+ if (this.isRunning) {
178
+ this.stop();
179
+ }
180
+
181
+ return this.start();
182
+ }
183
+
174
184
  stop(): void {
175
185
  this.#requireInstalled();
176
186
 
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: settings
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
+ ---
5
+
6
+ Read or change macroclaw settings. Only `model` and `timezone` can be changed through this skill.
7
+
8
+ ## Settings file
9
+
10
+ Location: `~/.macroclaw/settings.json`
11
+
12
+ ## Reading settings
13
+
14
+ When the user asks about current settings ("what model am I on?", "what's the timezone?"):
15
+
16
+ 1. Read `~/.macroclaw/settings.json`
17
+ 2. Report the requested value
18
+
19
+ ## Changing settings
20
+
21
+ Allowed changes:
22
+ - **model**: `haiku`, `sonnet`, or `opus`
23
+ - **timezone**: any valid IANA timezone (e.g. `Europe/Prague`, `America/New_York`, `UTC`)
24
+
25
+ All other settings (botToken, chatId, workspace, etc.) cannot be changed through this skill — tell the user to run `macroclaw setup` instead.
26
+
27
+ ## Procedure for changing a setting
28
+
29
+ Changing a setting requires a service restart, which kills the current process. Everything must happen in a single response — the same pattern as self-update.
30
+
31
+ 1. **Read current settings**: Read `~/.macroclaw/settings.json` and note the current value.
32
+
33
+ 2. **Validate**: Check the new value is valid (see allowed values above). If invalid, tell the user and stop.
34
+
35
+ 3. **Write**: Update the value in `~/.macroclaw/settings.json`, preserving all other fields. Write the file with `JSON.stringify(settings, null, 2)` formatting.
36
+
37
+ 4. **Generate LOG_FILE_PATH**: Run `echo "/tmp/macroclaw-restart-$(date -u +%Y-%m-%dT%H-%M-%SZ).log"` and use the output as `<LOG_FILE_PATH>` in the following steps.
38
+
39
+ 5. **Schedule follow-up**: Using the `schedule` skill, create a one-shot event 1 minute from now:
40
+ - Name: `settings-check`
41
+ - Prompt: `Check macroclaw restart after settings change. Read ~/.macroclaw/settings.json and confirm <setting>=<new-value>. Read <LOG_FILE_PATH> — if it contains "restarted", the restart succeeded. If the file doesn't exist or is empty, schedule another check in 1 minute. Run macroclaw service status to verify the service is running. Report the result.`
42
+ - Model: `haiku`
43
+ - Recurring: `false`
44
+
45
+ 6. **Run the restart**: Execute the restart script bundled with this skill:
46
+ ```
47
+ bash ${CLAUDE_SKILL_DIR}/scripts/restart.sh <LOG_FILE_PATH>
48
+ ```
49
+
50
+ ## Important
51
+
52
+ - The restart stops the service, which kills all processes including this Claude Code session.
53
+ - Do NOT use a background agent — it gets killed along with the main process.
54
+ - Always run step 6 LAST — everything after it may not execute.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ $# -ne 1 ]]; then
5
+ echo "Usage: $0 <log-file>" >&2
6
+ exit 1
7
+ fi
8
+
9
+ LOG_FILE="$1"
10
+
11
+ case "$(uname -s)" in
12
+ Linux)
13
+ systemd-run --user \
14
+ --unit="macroclaw-restart-$(date -u +%Y%m%dT%H%M%SZ)" \
15
+ --collect \
16
+ --no-block \
17
+ --setenv="PATH=$PATH" \
18
+ /bin/bash -lc "exec macroclaw service restart > \"$LOG_FILE\" 2>&1"
19
+ ;;
20
+ Darwin)
21
+ nohup setsid /bin/bash -lc "exec macroclaw service restart > \"$LOG_FILE\" 2>&1" >/dev/null 2>&1 &
22
+ ;;
23
+ *)
24
+ echo "Unsupported platform: $(uname -s)" >&2
25
+ exit 1
26
+ ;;
27
+ esac
@@ -57,15 +57,6 @@ Don't ask permission. Just do it.
57
57
  - bullet points (plain text bullet character)
58
58
  - No markdown syntax. No # headings. No [links](url). No *stars*.
59
59
 
60
- ## Scheduled Events
61
-
62
- Scheduled events are created by the `schedule` skill and stored in `data/schedule.json`. Messages prefixed with `[Context: cron/<name>]` are automated scheduled events. The agent decides whether to respond:
63
-
64
- - **action: "send"** — the response goes to Telegram
65
- - **action: "silent"** — the response is logged but not sent
66
-
67
- Use `silent` when a scheduled check finds nothing new. Only send when there's something worth reading.
68
-
69
60
  ## Skills
70
61
 
71
62
  Skills live in `.claude/skills/`.