macroclaw 0.9.0 → 0.10.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macroclaw",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Telegram-to-Claude-Code bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.test.ts CHANGED
@@ -115,6 +115,8 @@ function mockService(overrides?: Partial<SystemService>): SystemService {
115
115
  start: mock(() => ""),
116
116
  stop: mock(() => {}),
117
117
  update: mock(() => ""),
118
+ status: mock(() => ({ installed: false, running: false, platform: "systemd" as const })),
119
+ logs: mock(() => "journalctl -u macroclaw -n 50 --no-pager"),
118
120
  ...overrides,
119
121
  };
120
122
  }
@@ -155,6 +157,27 @@ describe("Cli.service", () => {
155
157
  expect(update).toHaveBeenCalled();
156
158
  });
157
159
 
160
+ it("runs status action", () => {
161
+ const status = mock(() => ({ installed: true, running: true, platform: "systemd" as const, pid: 42, uptime: "Thu 2026-03-12 10:00:00 UTC" }));
162
+ const cli = new Cli(undefined, mockService({ status }));
163
+ cli.service("status");
164
+ expect(status).toHaveBeenCalled();
165
+ });
166
+
167
+ it("runs logs action", () => {
168
+ const logs = mock(() => "journalctl -u macroclaw -n 50 --no-pager");
169
+ const cli = new Cli(undefined, mockService({ logs }));
170
+ cli.service("logs");
171
+ expect(logs).toHaveBeenCalledWith(undefined);
172
+ });
173
+
174
+ it("passes follow flag to logs action", () => {
175
+ const logs = mock(() => "journalctl -u macroclaw -f");
176
+ const cli = new Cli(undefined, mockService({ logs }));
177
+ cli.service("logs", undefined, true);
178
+ expect(logs).toHaveBeenCalledWith(true);
179
+ });
180
+
158
181
  it("throws for unknown action", () => {
159
182
  const cli = new Cli(undefined, mockService());
160
183
  expect(() => cli.service("bogus")).toThrow("Unknown service action: bogus");
package/src/cli.ts CHANGED
@@ -83,7 +83,7 @@ export class Cli {
83
83
  exec(args.join(" "), { cwd: settings.workspace, stdio: "inherit", env: { ...process.env, CLAUDECODE: "" } });
84
84
  }
85
85
 
86
- service(action: string, token?: string): void {
86
+ service(action: string, token?: string, follow?: boolean): void {
87
87
  switch (action) {
88
88
  case "install": {
89
89
  const logCmd = this.#systemService.install(token);
@@ -108,6 +108,23 @@ export class Cli {
108
108
  console.log(`Service updated. Check logs:\n ${logCmd}`);
109
109
  break;
110
110
  }
111
+ case "status": {
112
+ const s = this.#systemService.status();
113
+ const lines = [
114
+ `Platform: ${s.platform}`,
115
+ `Installed: ${s.installed ? "yes" : "no"}`,
116
+ `Running: ${s.running ? "yes" : "no"}`,
117
+ ];
118
+ if (s.pid) lines.push(`PID: ${s.pid}`);
119
+ if (s.uptime) lines.push(`Active since: ${s.uptime}`);
120
+ console.log(lines.join("\n"));
121
+ break;
122
+ }
123
+ case "logs": {
124
+ const cmd = this.#systemService.logs(follow);
125
+ console.log(cmd);
126
+ break;
127
+ }
111
128
  default:
112
129
  throw new Error(`Unknown service action: ${action}`);
113
130
  }
@@ -169,6 +186,19 @@ const serviceUpdateCommand = defineCommand({
169
186
  run: () => { try { defaultCli.service("update"); } catch (err) { handleError(err); } },
170
187
  });
171
188
 
189
+ const serviceStatusCommand = defineCommand({
190
+ meta: { name: "status", description: "Show service installation and running status" },
191
+ run: () => { try { defaultCli.service("status"); } catch (err) { handleError(err); } },
192
+ });
193
+
194
+ const serviceLogsCommand = defineCommand({
195
+ meta: { name: "logs", description: "Print the command to view service logs" },
196
+ args: {
197
+ follow: { type: "boolean", alias: "f", description: "Follow log output in real-time" },
198
+ },
199
+ run: ({ args }) => { try { defaultCli.service("logs", undefined, args.follow); } catch (err) { handleError(err); } },
200
+ });
201
+
172
202
  const serviceCommand = defineCommand({
173
203
  meta: { name: "service", description: "Manage macroclaw system service" },
174
204
  subCommands: {
@@ -177,6 +207,8 @@ const serviceCommand = defineCommand({
177
207
  start: serviceStartCommand,
178
208
  stop: serviceStopCommand,
179
209
  update: serviceUpdateCommand,
210
+ status: serviceStatusCommand,
211
+ logs: serviceLogsCommand,
180
212
  },
181
213
  });
182
214
 
@@ -588,3 +588,115 @@ describe("update", () => {
588
588
  }
589
589
  });
590
590
  });
591
+
592
+ describe("status", () => {
593
+ it("returns not installed, not running when service file missing", () => {
594
+ mockExistsSync.mockImplementation(() => false);
595
+ mockExecSync.mockImplementation((cmd: string) => {
596
+ if (cmd === "systemctl is-active macroclaw") throw new Error("not found");
597
+ return "";
598
+ });
599
+ const mgr = createManager();
600
+ const s = mgr.status();
601
+ expect(s.installed).toBe(false);
602
+ expect(s.running).toBe(false);
603
+ expect(s.platform).toBe("systemd");
604
+ expect(s.pid).toBeUndefined();
605
+ expect(s.uptime).toBeUndefined();
606
+ });
607
+
608
+ it("returns installed but not running for systemd", () => {
609
+ mockExistsSync.mockImplementation(() => true);
610
+ mockExecSync.mockImplementation((cmd: string) => {
611
+ if (cmd === "systemctl is-active macroclaw") return SYSTEMD_INACTIVE;
612
+ return "";
613
+ });
614
+ const mgr = createManager();
615
+ const s = mgr.status();
616
+ expect(s.installed).toBe(true);
617
+ expect(s.running).toBe(false);
618
+ expect(s.pid).toBeUndefined();
619
+ });
620
+
621
+ it("returns pid and uptime for running systemd service", () => {
622
+ mockExistsSync.mockImplementation(() => true);
623
+ mockExecSync.mockImplementation((cmd: string) => {
624
+ if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
625
+ if (cmd.startsWith("systemctl show macroclaw")) return "MainPID=42\nActiveEnterTimestamp=Thu 2026-03-12 10:00:00 UTC";
626
+ return "";
627
+ });
628
+ const mgr = createManager();
629
+ const s = mgr.status();
630
+ expect(s.installed).toBe(true);
631
+ expect(s.running).toBe(true);
632
+ expect(s.pid).toBe(42);
633
+ expect(s.uptime).toBe("Thu 2026-03-12 10:00:00 UTC");
634
+ });
635
+
636
+ it("returns pid for running launchd service", () => {
637
+ mockExistsSync.mockImplementation(() => true);
638
+ mockExecSync.mockImplementation((cmd: string) => {
639
+ if (cmd.startsWith("launchctl list ")) return LAUNCHD_RUNNING;
640
+ return "";
641
+ });
642
+ const mgr = createManager({ platform: "darwin" });
643
+ const s = mgr.status();
644
+ expect(s.installed).toBe(true);
645
+ expect(s.running).toBe(true);
646
+ expect(s.platform).toBe("launchd");
647
+ expect(s.pid).toBe(12345);
648
+ });
649
+
650
+ it("handles systemctl show failure gracefully", () => {
651
+ mockExistsSync.mockImplementation(() => true);
652
+ mockExecSync.mockImplementation((cmd: string) => {
653
+ if (cmd === "systemctl is-active macroclaw") return SYSTEMD_ACTIVE;
654
+ if (cmd.startsWith("systemctl show")) throw new Error("failed");
655
+ return "";
656
+ });
657
+ const mgr = createManager();
658
+ const s = mgr.status();
659
+ expect(s.running).toBe(true);
660
+ expect(s.pid).toBeUndefined();
661
+ expect(s.uptime).toBeUndefined();
662
+ });
663
+
664
+ it("handles launchctl list failure gracefully during status", () => {
665
+ mockExistsSync.mockImplementation(() => true);
666
+ let callCount = 0;
667
+ mockExecSync.mockImplementation((cmd: string) => {
668
+ if (cmd.startsWith("launchctl list ")) {
669
+ callCount++;
670
+ if (callCount === 1) return LAUNCHD_RUNNING;
671
+ throw new Error("failed");
672
+ }
673
+ return "";
674
+ });
675
+ const mgr = createManager({ platform: "darwin" });
676
+ const s = mgr.status();
677
+ expect(s.running).toBe(true);
678
+ expect(s.pid).toBeUndefined();
679
+ });
680
+ });
681
+
682
+ describe("logs", () => {
683
+ it("returns journalctl command for systemd", () => {
684
+ const mgr = createManager();
685
+ expect(mgr.logs()).toBe("journalctl -u macroclaw -n 50 --no-pager");
686
+ });
687
+
688
+ it("returns journalctl follow command for systemd", () => {
689
+ const mgr = createManager();
690
+ expect(mgr.logs(true)).toBe("journalctl -u macroclaw -f");
691
+ });
692
+
693
+ it("returns tail command for launchd", () => {
694
+ const mgr = createManager({ platform: "darwin" });
695
+ expect(mgr.logs()).toBe("tail -n 50 /home/testuser/.macroclaw/logs/stdout.log");
696
+ });
697
+
698
+ it("returns tail follow command for launchd", () => {
699
+ const mgr = createManager({ platform: "darwin" });
700
+ expect(mgr.logs(true)).toBe("tail -f /home/testuser/.macroclaw/logs/stdout.log /home/testuser/.macroclaw/logs/stderr.log");
701
+ });
702
+ });
package/src/service.ts CHANGED
@@ -50,12 +50,22 @@ interface LinuxUser {
50
50
  home: string;
51
51
  }
52
52
 
53
+ export interface ServiceStatus {
54
+ installed: boolean;
55
+ running: boolean;
56
+ platform: Platform;
57
+ pid?: number;
58
+ uptime?: string;
59
+ }
60
+
53
61
  export interface SystemService {
54
62
  install: (oauthToken?: string) => string;
55
63
  uninstall: () => void;
56
64
  start: () => string;
57
65
  stop: () => void;
58
66
  update: () => string;
67
+ status: () => ServiceStatus;
68
+ logs: (follow?: boolean) => string;
59
69
  }
60
70
 
61
71
  export class ServiceManager implements SystemService {
@@ -229,6 +239,46 @@ export class ServiceManager implements SystemService {
229
239
  return this.#logTailCommand();
230
240
  }
231
241
 
242
+ status(): ServiceStatus {
243
+ const result: ServiceStatus = {
244
+ installed: this.isInstalled,
245
+ running: this.isRunning,
246
+ platform: this.#platform,
247
+ };
248
+
249
+ if (result.running) {
250
+ if (this.#platform === "launchd") {
251
+ try {
252
+ const out = this.#deps.execSync(`launchctl list ${LAUNCHD_LABEL}`);
253
+ const pidMatch = /"PID"\s*=\s*(\d+)/.exec(out);
254
+ if (pidMatch) result.pid = Number(pidMatch[1]);
255
+ } catch { /* best effort */ }
256
+ } else {
257
+ try {
258
+ const out = this.#deps.execSync("systemctl show macroclaw --property=MainPID,ActiveEnterTimestamp --no-pager");
259
+ const pidMatch = /MainPID=(\d+)/.exec(out);
260
+ if (pidMatch && pidMatch[1] !== "0") result.pid = Number(pidMatch[1]);
261
+ const tsMatch = /ActiveEnterTimestamp=(.+)/.exec(out);
262
+ if (tsMatch?.[1].trim()) result.uptime = tsMatch[1].trim();
263
+ } catch { /* best effort */ }
264
+ }
265
+ }
266
+
267
+ return result;
268
+ }
269
+
270
+ logs(follow = false): string {
271
+ if (this.#platform === "launchd") {
272
+ const logDir = resolve(this.#deps.home, ".macroclaw/logs");
273
+ return follow
274
+ ? `tail -f ${logDir}/stdout.log ${logDir}/stderr.log`
275
+ : `tail -n 50 ${logDir}/stdout.log`;
276
+ }
277
+ return follow
278
+ ? "journalctl -u macroclaw -f"
279
+ : "journalctl -u macroclaw -n 50 --no-pager";
280
+ }
281
+
232
282
  #resolvePath(binary: string): string {
233
283
  try {
234
284
  return this.#deps.execSync(`which ${binary}`).trim();