macroclaw 0.35.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 +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/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
|