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 +9 -3
- package/package.json +1 -1
- package/src/cli.test.ts +8 -0
- package/src/cli.ts +11 -0
- package/src/system-service.test.ts +44 -0
- package/src/system-service.ts +10 -0
- package/workspace-template/.claude/skills/settings/SKILL.md +54 -0
- package/workspace-template/.claude/skills/settings/scripts/restart.sh +27 -0
- package/workspace-template/CLAUDE.md +0 -9
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` | `
|
|
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
|
|
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
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" });
|
package/src/system-service.ts
CHANGED
|
@@ -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/`.
|